• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

future-architect / uroborosql / #770

12 Sep 2024 03:28PM UTC coverage: 90.35% (-0.9%) from 91.21%
#770

push

web-flow
 Enable per-SQL-ID log suppression (#322) (#332)

* 各Logger用のインタフェースを作成し必要なクラスがimplementsするように修正。そのうえでSLF4J 2.xのAPIを利用するように変更(性能改善)

* Changed to use slf4j v2.0 API
Also added suppressLogging API

* Refactoring log-related class and method names.

384 of 587 new or added lines in 32 files covered. (65.42%)

9 existing lines in 7 files now uncovered.

8885 of 9834 relevant lines covered (90.35%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.37
/src/main/java/jp/co/future/uroborosql/event/subscriber/AbstractSecretColumnEventSubscriber.java
1
/**
2
 * Copyright (c) 2017-present, Future Corporation
3
 *
4
 * This source code is licensed under the MIT license found in the
5
 * LICENSE file in the root directory of this source tree.
6
 */
7
package jp.co.future.uroborosql.event.subscriber;
8

9
import java.io.BufferedInputStream;
10
import java.nio.charset.Charset;
11
import java.nio.charset.StandardCharsets;
12
import java.nio.charset.UnsupportedCharsetException;
13
import java.nio.file.Files;
14
import java.nio.file.Path;
15
import java.nio.file.Paths;
16
import java.security.GeneralSecurityException;
17
import java.security.KeyStore;
18
import java.security.KeyStore.SecretKeyEntry;
19
import java.util.ArrayList;
20
import java.util.Base64;
21
import java.util.List;
22
import java.util.Optional;
23
import java.util.function.Function;
24

25
import javax.crypto.Cipher;
26
import javax.crypto.SecretKey;
27
import javax.crypto.spec.IvParameterSpec;
28

29
import org.slf4j.Logger;
30

31
import jp.co.future.uroborosql.event.AfterSqlQueryEvent;
32
import jp.co.future.uroborosql.event.BeforeSetParameterEvent;
33
import jp.co.future.uroborosql.log.support.EventLoggingSupport;
34
import jp.co.future.uroborosql.log.support.SettingLoggingSupport;
35
import jp.co.future.uroborosql.parameter.Parameter;
36
import jp.co.future.uroborosql.utils.CaseFormat;
37
import jp.co.future.uroborosql.utils.ObjectUtils;
38

39
/**
40
 * 特定のカラムの読み書きに対して暗号化/復号化を行うイベントサブスクライバの抽象クラス.
41
 *
42
 * 登録、更新時はパラメータを暗号化 検索時は検索結果を復号化する
43
 *
44
 * @param <T> SecretColumnEventSubscriberの具象型
45
 * @author H.Sugimoto
46
 * @since v1.0.0
47
 *
48
 */
49
public abstract class AbstractSecretColumnEventSubscriber<T> extends EventSubscriber
50
                implements EventLoggingSupport, SettingLoggingSupport {
51
        /** イベントロガー */
52
        private static final Logger EVENT_LOG = EventLoggingSupport.getEventLogger("secretcolumn");
1✔
53

54
        /** 暗号キー */
55
        private SecretKey secretKey = null;
1✔
56

57
        /** 暗号器 */
58
        private Cipher encryptCipher = null;
1✔
59

60
        /** 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。 */
61
        private String keyStoreFilePath = null;
1✔
62

63
        /** KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定する */
64
        private String storePassword = null;
1✔
65

66
        /** KeyStore内で秘密鍵が格納されている場所を示すエイリアス名 */
67
        private String alias = null;
1✔
68

69
        /** キャラクタセット(デフォルトUTF-8) */
70
        private Charset charset = StandardCharsets.UTF_8;
1✔
71

72
        /** 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定する */
73
        private List<String> cryptColumnNames = null;
1✔
74

75
        /** 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存される */
76
        private List<String> cryptParamKeys = null;
1✔
77

78
        /** IVを利用するかどうか */
79
        private boolean useIV = false;
1✔
80

81
        private boolean skip = false;
1✔
82

83
        /**
84
         * 変換の名前 (たとえば、DES/CBC/PKCS5Padding)。標準の変換名については、Java 暗号化アーキテクチャー標準アルゴリズム名のドキュメントの Cipher のセクションを参照。
85
         * 初期値は<code>AES/ECB/PKCS5Padding</code>
86
         */
87
        private String transformationType = "AES/ECB/PKCS5Padding";
1✔
88

89
        protected AbstractSecretColumnEventSubscriber() {
1✔
90
        }
1✔
91

92
        /**
93
         *
94
         * {@inheritDoc}
95
         *
96
         * @see jp.co.future.uroborosql.event.subscriber.EventSubscriber#initialize()
97
         */
98
        @Override
99
        public void initialize() {
100
                if (getCryptColumnNames() == null || getCryptColumnNames().isEmpty()) {
1✔
101
                        setSkip(true);
1✔
102
                        return;
1✔
103
                } else {
104
                        cryptParamKeys = new ArrayList<>();
1✔
105
                        var newColumnNames = new ArrayList<String>();
1✔
106
                        for (var columnName : getCryptColumnNames()) {
1✔
107
                                cryptParamKeys.add(CaseFormat.CAMEL_CASE.convert(columnName));
1✔
108
                                newColumnNames.add(CaseFormat.UPPER_SNAKE_CASE.convert(columnName));
1✔
109
                        }
1✔
110
                        // 定義ファイルで指定されたカラム名は大文字でない可能性があるので、ここで大文字に置換し直す
111
                        cryptColumnNames = newColumnNames;
1✔
112
                }
113

114
                KeyStore store;
115
                try {
116
                        if (ObjectUtils.isBlank(getKeyStoreFilePath())) {
1✔
117
                                errorWith(SETTING_LOG)
1✔
118
                                                .setMessage("Invalid KeyStore file path. Path:{}")
1✔
119
                                                .addArgument(getKeyStoreFilePath())
1✔
120
                                                .log();
1✔
121
                                setSkip(true);
1✔
122
                                return;
1✔
123
                        }
124
                        var storeFile = toPath(getKeyStoreFilePath());
1✔
125
                        if (!Files.exists(storeFile)) {
1✔
126
                                errorWith(SETTING_LOG)
1✔
127
                                                .setMessage("Not found KeyStore file path. Path:{}")
1✔
128
                                                .addArgument(getKeyStoreFilePath())
1✔
129
                                                .log();
1✔
130
                                setSkip(true);
1✔
131
                                return;
1✔
132
                        }
133
                        if (Files.isDirectory(storeFile)) {
1✔
134
                                errorWith(SETTING_LOG)
1✔
135
                                                .setMessage("Invalid KeyStore file path. Path:{}")
1✔
136
                                                .addArgument(getKeyStoreFilePath())
1✔
137
                                                .log();
1✔
138
                                setSkip(true);
1✔
139
                                return;
1✔
140
                        }
141
                        if (ObjectUtils.isBlank(getStorePassword())) {
1✔
142
                                errorWith(SETTING_LOG)
1✔
143
                                                .setMessage("Invalid password for access KeyStore.")
1✔
144
                                                .log();
1✔
145
                                setSkip(true);
1✔
146
                                return;
1✔
147
                        }
148
                        if (ObjectUtils.isBlank(getAlias())) {
1✔
149
                                errorWith(SETTING_LOG)
1✔
150
                                                .setMessage("No alias for access KeyStore.")
1✔
151
                                                .log();
1✔
152
                                setSkip(true);
1✔
153
                                return;
1✔
154
                        }
155

156
                        store = KeyStore.getInstance("JCEKS");
1✔
157

158
                        char[] pass;
159
                        try (var is = new BufferedInputStream(Files.newInputStream(storeFile))) {
1✔
160
                                pass = new String(Base64.getUrlDecoder().decode(getStorePassword())).toCharArray();
1✔
161

162
                                store.load(is, pass);
1✔
163
                        }
164

165
                        var entry = (SecretKeyEntry) store.getEntry(getAlias(),
1✔
166
                                        new KeyStore.PasswordProtection(pass));
167

168
                        secretKey = entry.getSecretKey();
1✔
169
                        encryptCipher = Cipher.getInstance(transformationType);
1✔
170
                        encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey);
1✔
171
                        useIV = encryptCipher.getIV() != null;
1✔
172
                } catch (Exception ex) {
×
NEW
173
                        errorWith(SETTING_LOG)
×
NEW
174
                                        .setMessage("Failed to acquire secret key.")
×
NEW
175
                                        .setCause(ex)
×
NEW
176
                                        .log();
×
UNCOV
177
                        setSkip(true);
×
178
                }
1✔
179

180
                beforeSetParameterListener(this::beforeSetParameter);
1✔
181
                afterSqlQueryListener(this::afterSqlQuery);
1✔
182
        }
1✔
183

184
        void beforeSetParameter(final BeforeSetParameterEvent evt) {
185
                // パラメータが暗号化対象のパラメータ名と一致する場合、パラメータの値を暗号化する
186
                var parameter = evt.getParameter();
1✔
187
                if (skip || parameter == null) {
1✔
188
                        return;
1✔
189
                }
190

191
                if (Parameter.class.equals(parameter.getClass())) {
1✔
192
                        // 通常のパラメータの場合
193
                        var key = parameter.getParameterName();
1✔
194
                        if (getCryptParamKeys().contains(CaseFormat.CAMEL_CASE.convert(key))) {
1✔
195
                                var obj = parameter.getValue();
1✔
196
                                if (obj != null) {
1✔
197
                                        String objStr = null;
1✔
198
                                        if (obj instanceof Optional) {
1✔
199
                                                objStr = ((Optional<?>) obj)
1✔
200
                                                                .map(Object::toString)
1✔
201
                                                                .orElse(null);
1✔
202
                                        } else {
203
                                                objStr = obj.toString();
1✔
204
                                        }
205
                                        if (ObjectUtils.isNotEmpty(objStr)) {
1✔
206
                                                try {
207
                                                        synchronized (encryptCipher) {
1✔
208
                                                                evt.setParameter(
1✔
209
                                                                                new Parameter(key, encrypt(encryptCipher, secretKey, objStr)));
1✔
210
                                                        }
1✔
211
                                                } catch (Exception ex) {
×
NEW
212
                                                        warnWith(EVENT_LOG)
×
NEW
213
                                                                        .setMessage("Encrypt Exception key:{}")
×
NEW
214
                                                                        .addArgument(key)
×
NEW
215
                                                                        .log();
×
216
                                                }
1✔
217
                                        }
218
                                }
219
                        }
220
                }
221

222
        }
1✔
223

224
        void afterSqlQuery(final AfterSqlQueryEvent evt) {
225
                // 検索結果に暗号化対象カラムが含まれる場合、値の取得時に復号化されるようResultSetを SecretResultSet でラップして返す
226
                if (skip) {
1✔
227
                        return;
1✔
228
                }
229

230
                try {
231
                        evt.setResultSet(new SecretResultSet(evt.getResultSet(), this.createDecryptor(),
1✔
232
                                        getCryptColumnNames(), getCharset()));
1✔
233
                } catch (Exception ex) {
×
NEW
234
                        errorWith(EVENT_LOG)
×
NEW
235
                                        .setMessage("Failed to create SecretResultSet.")
×
NEW
236
                                        .setCause(ex)
×
NEW
237
                                        .log();
×
238
                }
1✔
239
        }
1✔
240

241
        /**
242
         * パラメータの暗号化内部処理。
243
         *
244
         * @param cipher 暗号器
245
         * @param secretKey 暗号化キー
246
         * @param input 暗号化対象文字列
247
         * @return 暗号化後文字列
248
         * @throws GeneralSecurityException サブクラスでの拡張に備えて javax.crypto パッケージ配下の例外の親クラス
249
         */
250
        protected abstract String encrypt(final Cipher cipher, final SecretKey secretKey, final String input)
251
                        throws GeneralSecurityException;
252

253
        /**
254
         * パラメータの復号化内部処理。
255
         *
256
         * @param cipher 暗号器
257
         * @param secretKey 暗号化キー
258
         * @param secret 暗号化文字列
259
         * @return 平文文字列
260
         * @throws GeneralSecurityException サブクラスでの拡張に備えて javax.crypto パッケージ配下の例外の親クラス
261
         */
262
        protected abstract String decrypt(final Cipher cipher, final SecretKey secretKey, final String secret)
263
                        throws GeneralSecurityException;
264

265
        /**
266
         * {@link SecretResultSet} が復号に使用するラムダを構築する。
267
         *
268
         * @return 暗号を受け取り平文を返すラムダ
269
         * @throws GeneralSecurityException サブクラスでの拡張に備えて javax.crypto パッケージ配下の例外の親クラス
270
         */
271
        private Function<Object, String> createDecryptor() throws GeneralSecurityException {
272
                var cipher = Cipher.getInstance(transformationType);
1✔
273
                if (useIV) {
1✔
274
                        cipher.init(Cipher.DECRYPT_MODE, secretKey,
1✔
275
                                        encryptCipher.getParameters().getParameterSpec(IvParameterSpec.class));
1✔
276
                } else {
277
                        cipher.init(Cipher.DECRYPT_MODE, secretKey);
1✔
278
                }
279

280
                return secret -> {
1✔
281
                        if (secret == null) {
1✔
282
                                return null;
1✔
283
                        }
284

285
                        var secretStr = secret.toString();
1✔
286
                        if (ObjectUtils.isNotEmpty(secretStr)) {
1✔
287
                                synchronized (cipher) {
1✔
288
                                        try {
289
                                                return decrypt(cipher, secretKey, secretStr);
1✔
290
                                        } catch (Exception ex) {
1✔
291
                                                return secretStr;
1✔
292
                                        }
293
                                }
294
                        } else {
295
                                return secretStr;
×
296
                        }
297
                };
298
        }
299

300
        /**
301
         * 文字列からPathオブジェクトに変換する。 Pathの取得方法をカスタマイズしたい場合にオーバーライドする。
302
         *
303
         * @param path パス文字列
304
         * @return Pathオブジェクト
305
         */
306
        protected Path toPath(final String path) {
307
                return Paths.get(path);
1✔
308
        }
309

310
        /**
311
         * 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。を取得します。
312
         *
313
         * @return 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。
314
         */
315
        public String getKeyStoreFilePath() {
316
                return keyStoreFilePath;
1✔
317
        }
318

319
        /**
320
         * 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。を設定します。
321
         *
322
         * @param keyStoreFilePath 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。
323
         * @return 具象型のインスタンス
324
         */
325
        @SuppressWarnings("unchecked")
326
        public T setKeyStoreFilePath(final String keyStoreFilePath) {
327
                this.keyStoreFilePath = keyStoreFilePath;
1✔
328
                return (T) this;
1✔
329
        }
330

331
        /**
332
         * KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定するを取得します。
333
         *
334
         * @return KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定する
335
         */
336
        public String getStorePassword() {
337
                return storePassword;
1✔
338
        }
339

340
        /**
341
         * KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定するを設定します。
342
         *
343
         * @param storePassword KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定する
344
         * @return 具象型のインスタンス
345
         */
346
        @SuppressWarnings("unchecked")
347
        public T setStorePassword(final String storePassword) {
348
                this.storePassword = storePassword;
1✔
349
                return (T) this;
1✔
350
        }
351

352
        /**
353
         * KeyStore内で秘密鍵が格納されている場所を示すエイリアス名を取得します。
354
         *
355
         * @return KeyStore内で秘密鍵が格納されている場所を示すエイリアス名
356
         */
357
        public String getAlias() {
358
                return alias;
1✔
359
        }
360

361
        /**
362
         * KeyStore内で秘密鍵が格納されている場所を示すエイリアス名を設定します。
363
         *
364
         * @param alias KeyStore内で秘密鍵が格納されている場所を示すエイリアス名
365
         * @return 具象型のインスタンス
366
         */
367
        @SuppressWarnings("unchecked")
368
        public T setAlias(final String alias) {
369
                this.alias = alias;
1✔
370
                return (T) this;
1✔
371
        }
372

373
        /**
374
         * キャラクタセット(デフォルトUTF-8)を取得します。
375
         *
376
         * @return キャラクタセット(デフォルトUTF-8)
377
         */
378
        public Charset getCharset() {
379
                return charset;
1✔
380
        }
381

382
        /**
383
         * キャラクタセット(デフォルトUTF-8)を設定します。
384
         *
385
         * @param charset キャラクタセット(デフォルトUTF-8)
386
         * @return 具象型のインスタンス
387
         */
388
        @SuppressWarnings("unchecked")
389
        public T setCharset(final String charset) {
390
                try {
391
                        this.charset = Charset.forName(charset);
1✔
392
                } catch (UnsupportedCharsetException ex) {
×
393
                        this.charset = StandardCharsets.UTF_8;
×
NEW
394
                        errorWith(SETTING_LOG)
×
NEW
395
                                        .setMessage(
×
396
                                                        "The specified character set could not be converted to {}. Set the default character set({}).")
NEW
397
                                        .addArgument(charset)
×
NEW
398
                                        .addArgument(this.charset)
×
NEW
399
                                        .log();
×
400
                }
1✔
401
                return (T) this;
1✔
402
        }
403

404
        /**
405
         * 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定するを取得します。
406
         *
407
         * @return 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定する
408
         */
409
        public List<String> getCryptColumnNames() {
410
                return cryptColumnNames;
1✔
411
        }
412

413
        /**
414
         * 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定するを設定します。
415
         *
416
         * @param cryptColumnNames 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定する
417
         * @return 具象型のインスタンス
418
         */
419
        @SuppressWarnings("unchecked")
420
        public T setCryptColumnNames(final List<String> cryptColumnNames) {
421
                this.cryptColumnNames = cryptColumnNames;
1✔
422
                return (T) this;
1✔
423
        }
424

425
        /**
426
         * 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存されるを取得します。
427
         *
428
         * @return 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存される
429
         */
430
        protected List<String> getCryptParamKeys() {
431
                return cryptParamKeys;
1✔
432
        }
433

434
        /**
435
         * 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存されるを設定します。
436
         *
437
         * @param cryptParamKeys 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存される
438
         */
439
        protected void setCryptParamKeys(final List<String> cryptParamKeys) {
440
                this.cryptParamKeys = cryptParamKeys;
1✔
441
        }
1✔
442

443
        /**
444
         * skipを取得します。
445
         *
446
         * @return skip
447
         */
448
        public boolean isSkip() {
449
                return skip;
1✔
450
        }
451

452
        /**
453
         * skipを設定します。
454
         *
455
         * @param skip skip
456
         * @return 具象型のインスタンス
457
         */
458
        @SuppressWarnings("unchecked")
459
        public T setSkip(final boolean skip) {
460
                this.skip = skip;
1✔
461
                return (T) this;
1✔
462
        }
463

464
        /**
465
         * 暗号キーを取得します。
466
         *
467
         * @return 暗号キー
468
         */
469
        protected SecretKey getSecretKey() {
470
                return secretKey;
1✔
471
        }
472

473
        /**
474
         * IVを利用するかどうかを取得します。
475
         *
476
         * @return IVを利用するかどうか
477
         */
478
        protected boolean isUseIV() {
479
                return useIV;
1✔
480
        }
481

482
        /**
483
         * 変換の名前を取得する 標準の変換名については、Java 暗号化アーキテクチャー標準アルゴリズム名のドキュメントの Cipher のセクションを参照。 初期値は<code>AES/ECB/PKCS5Padding</code>
484
         *
485
         * @return 変換の名前
486
         */
487
        public String getTransformationType() {
488
                return transformationType;
1✔
489
        }
490

491
        /**
492
         * 変換の名前を設定する 標準の変換名については、Java 暗号化アーキテクチャー標準アルゴリズム名のドキュメントの Cipher のセクションを参照。 初期値は<code>AES/ECB/PKCS5Padding</code>
493
         *
494
         * @param transformationType 変換の名前
495
         * @return 具象型のインスタンス
496
         */
497
        @SuppressWarnings("unchecked")
498
        public T setTransformationType(final String transformationType) {
499
                this.transformationType = transformationType;
1✔
500
                return (T) this;
1✔
501
        }
502

503
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc