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

future-architect / uroborosql / #743

28 Jun 2024 04:17PM UTC coverage: 90.988% (+0.5%) from 90.484%
#743

push

web-flow
add paramIfNotEmpty method (#318)

* v0.x to master merge

* fix nanoseconds diff (java8 to java11)

* eclipse cleanup and  optimize import

* eclipse format

* optimize Imports

* remove unused annotation

* library version up

* migrate v0.x to master
- update java version 8 to 11
- update junit4 to junit5
- library version update

* fix test failed

* remove unused annotation

* fixed bug

* use var

* fix java8 coding style

* Refactoring TransactionContextManager

* Refactoring SqlAgent

* 途中コミット

* fix testcase

* fix review

* fix javadoc comments

* merge v0.x PR

* cleanup code

* cleanup test code

* change build status badge

* - agent.query, update, proc, batch にそれぞれSupplierを引数にとるメソッドを追加
- SqlQuery.paramとSqlEntityUpdate.setにFunctionを受け取るメソッドを追加

* testcaseの整形

* - SqlFluent と ExtractionCondition の分離
- Arrays.asList -> List.of への変更
- テストの整形

* - v0.x系の不具合対応の追いつき
- ログ出力の整理

* - SqlKindの整理(ENTITY_XXXの追加)
- REPL_LOGの追加
- Deprecatedメソッドの削除(SqlAgent)
- SqlAgent, ExecutionContextでsetterをfluent APIに変更

* DB接続URLの修正

* add and fix testcases.

* add event testcases.

* fix typo

* add paramIfNotEmpty method and StringUtils rename ObjectUtils

* fix review comments.

* remove unused import

1695 of 1958 new or added lines in 97 files covered. (86.57%)

26 existing lines in 10 files now uncovered.

8249 of 9066 relevant lines covered (90.99%)

0.91 hits per line

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

91.43
/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
import org.slf4j.LoggerFactory;
31

32
import jp.co.future.uroborosql.event.BeforeSetParameterEvent;
33
import jp.co.future.uroborosql.event.SqlQueryEvent;
34
import jp.co.future.uroborosql.parameter.Parameter;
35
import jp.co.future.uroborosql.utils.CaseFormat;
36
import jp.co.future.uroborosql.utils.ObjectUtils;
37

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

52
        /** 設定ロガー */
53
        private static final Logger SETTING_LOG = LoggerFactory.getLogger("jp.co.future.uroborosql.setting");
1✔
54

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

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

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

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

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

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

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

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

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

82
        private boolean skip = false;
1✔
83

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

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

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

115
                KeyStore store;
116
                try {
117
                        if (ObjectUtils.isBlank(getKeyStoreFilePath())) {
1✔
118
                                SETTING_LOG.error("Invalid KeyStore file path. Path:{}", getKeyStoreFilePath());
1✔
119
                                setSkip(true);
1✔
120
                                return;
1✔
121
                        }
122
                        var storeFile = toPath(getKeyStoreFilePath());
1✔
123
                        if (!Files.exists(storeFile)) {
1✔
124
                                SETTING_LOG.error("Not found KeyStore file path. Path:{}", getKeyStoreFilePath());
1✔
125
                                setSkip(true);
1✔
126
                                return;
1✔
127
                        }
128
                        if (Files.isDirectory(storeFile)) {
1✔
129
                                SETTING_LOG.error("Invalid KeyStore file path. Path:{}", getKeyStoreFilePath());
1✔
130
                                setSkip(true);
1✔
131
                                return;
1✔
132
                        }
133
                        if (ObjectUtils.isBlank(getStorePassword())) {
1✔
134
                                SETTING_LOG.error("Invalid password for access KeyStore.");
1✔
135
                                setSkip(true);
1✔
136
                                return;
1✔
137
                        }
138
                        if (ObjectUtils.isBlank(getAlias())) {
1✔
139
                                SETTING_LOG.error("No alias for access KeyStore.");
1✔
140
                                setSkip(true);
1✔
141
                                return;
1✔
142
                        }
143

144
                        store = KeyStore.getInstance("JCEKS");
1✔
145

146
                        char[] pass;
147
                        try (var is = new BufferedInputStream(Files.newInputStream(storeFile))) {
1✔
148
                                pass = new String(Base64.getUrlDecoder().decode(getStorePassword())).toCharArray();
1✔
149

150
                                store.load(is, pass);
1✔
151
                        }
152

153
                        var entry = (SecretKeyEntry) store.getEntry(getAlias(),
1✔
154
                                        new KeyStore.PasswordProtection(pass));
155

156
                        secretKey = entry.getSecretKey();
1✔
157
                        encryptCipher = Cipher.getInstance(transformationType);
1✔
158
                        encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey);
1✔
159
                        useIV = encryptCipher.getIV() != null;
1✔
160
                } catch (Exception ex) {
×
NEW
161
                        SETTING_LOG.error("Failed to acquire secret key.", ex);
×
NEW
162
                        setSkip(true);
×
163
                }
1✔
164

165
                beforeSetParameterListener(this::beforeSetParameter);
1✔
166
                sqlQueryListener(this::sqlQuery);
1✔
167
        }
1✔
168

169
        void beforeSetParameter(final BeforeSetParameterEvent evt) {
170
                // パラメータが暗号化対象のパラメータ名と一致する場合、パラメータの値を暗号化する
171
                var parameter = evt.getParameter();
1✔
172
                if (skip || parameter == null) {
1✔
173
                        return;
1✔
174
                }
175

176
                if (Parameter.class.equals(parameter.getClass())) {
1✔
177
                        // 通常のパラメータの場合
178
                        var key = parameter.getParameterName();
1✔
179
                        if (getCryptParamKeys().contains(CaseFormat.CAMEL_CASE.convert(key))) {
1✔
180
                                var obj = parameter.getValue();
1✔
181
                                if (obj != null) {
1✔
182
                                        String objStr = null;
1✔
183
                                        if (obj instanceof Optional) {
1✔
184
                                                objStr = ((Optional<?>) obj)
1✔
185
                                                                .map(Object::toString)
1✔
186
                                                                .orElse(null);
1✔
187
                                        } else {
188
                                                objStr = obj.toString();
1✔
189
                                        }
190
                                        if (ObjectUtils.isNotEmpty(objStr)) {
1✔
191
                                                try {
192
                                                        synchronized (encryptCipher) {
1✔
193
                                                                evt.setParameter(
1✔
194
                                                                                new Parameter(key, encrypt(encryptCipher, secretKey, objStr)));
1✔
195
                                                        }
1✔
196
                                                } catch (Exception ex) {
×
NEW
197
                                                        if (EVENT_LOG.isWarnEnabled()) {
×
NEW
198
                                                                EVENT_LOG.warn("Encrypt Exception key:{}", key);
×
199
                                                        }
200
                                                }
1✔
201
                                        }
202
                                }
203
                        }
204
                }
205
        }
1✔
206

207
        void sqlQuery(final SqlQueryEvent evt) {
208
                // 検索結果に暗号化対象カラムが含まれる場合、値の取得時に復号化されるようResultSetを SecretResultSet でラップして返す
209
                if (skip) {
1✔
210
                        return;
1✔
211
                }
212

213
                try {
214
                        evt.setResultSet(new SecretResultSet(evt.getResultSet(), this.createDecryptor(),
1✔
215
                                        getCryptColumnNames(), getCharset()));
1✔
NEW
216
                } catch (Exception ex) {
×
NEW
217
                        EVENT_LOG.error("Failed to create SecretResultSet.", ex);
×
218
                }
1✔
219
        }
1✔
220

221
        /**
222
         * パラメータの暗号化内部処理。
223
         *
224
         * @param cipher 暗号器
225
         * @param secretKey 暗号化キー
226
         * @param input 暗号化対象文字列
227
         * @return 暗号化後文字列
228
         * @throws GeneralSecurityException サブクラスでの拡張に備えて javax.crypto パッケージ配下の例外の親クラス
229
         */
230
        protected abstract String encrypt(final Cipher cipher, final SecretKey secretKey, final String input)
231
                        throws GeneralSecurityException;
232

233
        /**
234
         * パラメータの復号化内部処理。
235
         *
236
         * @param cipher 暗号器
237
         * @param secretKey 暗号化キー
238
         * @param secret 暗号化文字列
239
         * @return 平文文字列
240
         * @throws GeneralSecurityException サブクラスでの拡張に備えて javax.crypto パッケージ配下の例外の親クラス
241
         */
242
        protected abstract String decrypt(final Cipher cipher, final SecretKey secretKey, final String secret)
243
                        throws GeneralSecurityException;
244

245
        /**
246
         * {@link SecretResultSet} が復号に使用するラムダを構築する。
247
         *
248
         * @return 暗号を受け取り平文を返すラムダ
249
         * @throws GeneralSecurityException サブクラスでの拡張に備えて javax.crypto パッケージ配下の例外の親クラス
250
         */
251
        private Function<Object, String> createDecryptor() throws GeneralSecurityException {
252
                var cipher = Cipher.getInstance(transformationType);
1✔
253
                if (useIV) {
1✔
254
                        cipher.init(Cipher.DECRYPT_MODE, secretKey,
1✔
255
                                        encryptCipher.getParameters().getParameterSpec(IvParameterSpec.class));
1✔
256
                } else {
257
                        cipher.init(Cipher.DECRYPT_MODE, secretKey);
1✔
258
                }
259

260
                return secret -> {
1✔
261
                        if (secret == null) {
1✔
262
                                return null;
1✔
263
                        }
264

265
                        var secretStr = secret.toString();
1✔
266
                        if (ObjectUtils.isNotEmpty(secretStr)) {
1✔
267
                                synchronized (cipher) {
1✔
268
                                        try {
269
                                                return decrypt(cipher, secretKey, secretStr);
1✔
270
                                        } catch (Exception ex) {
1✔
271
                                                return secretStr;
1✔
272
                                        }
273
                                }
274
                        } else {
275
                                return secretStr;
×
276
                        }
277
                };
278
        }
279

280
        /**
281
         * 文字列からPathオブジェクトに変換する。 Pathの取得方法をカスタマイズしたい場合にオーバーライドする。
282
         *
283
         * @param path パス文字列
284
         * @return Pathオブジェクト
285
         */
286
        protected Path toPath(final String path) {
287
                return Paths.get(path);
1✔
288
        }
289

290
        /**
291
         * 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。を取得します。
292
         *
293
         * @return 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。
294
         */
295
        public String getKeyStoreFilePath() {
296
                return keyStoreFilePath;
1✔
297
        }
298

299
        /**
300
         * 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。を設定します。
301
         *
302
         * @param keyStoreFilePath 秘密鍵を格納したKeyStoreファイルのパス. KeyStoreはJCEKSタイプであること。
303
         * @return 具象型のインスタンス
304
         */
305
        @SuppressWarnings("unchecked")
306
        public T setKeyStoreFilePath(final String keyStoreFilePath) {
307
                this.keyStoreFilePath = keyStoreFilePath;
1✔
308
                return (T) this;
1✔
309
        }
310

311
        /**
312
         * KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定するを取得します。
313
         *
314
         * @return KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定する
315
         */
316
        public String getStorePassword() {
317
                return storePassword;
1✔
318
        }
319

320
        /**
321
         * KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定するを設定します。
322
         *
323
         * @param storePassword KeyStoreにアクセスするためのストアパスワード. Base64エンコードした値を指定する
324
         * @return 具象型のインスタンス
325
         */
326
        @SuppressWarnings("unchecked")
327
        public T setStorePassword(final String storePassword) {
328
                this.storePassword = storePassword;
1✔
329
                return (T) this;
1✔
330
        }
331

332
        /**
333
         * KeyStore内で秘密鍵が格納されている場所を示すエイリアス名を取得します。
334
         *
335
         * @return KeyStore内で秘密鍵が格納されている場所を示すエイリアス名
336
         */
337
        public String getAlias() {
338
                return alias;
1✔
339
        }
340

341
        /**
342
         * KeyStore内で秘密鍵が格納されている場所を示すエイリアス名を設定します。
343
         *
344
         * @param alias KeyStore内で秘密鍵が格納されている場所を示すエイリアス名
345
         * @return 具象型のインスタンス
346
         */
347
        @SuppressWarnings("unchecked")
348
        public T setAlias(final String alias) {
349
                this.alias = alias;
1✔
350
                return (T) this;
1✔
351
        }
352

353
        /**
354
         * キャラクタセット(デフォルトUTF-8)を取得します。
355
         *
356
         * @return キャラクタセット(デフォルトUTF-8)
357
         */
358
        public Charset getCharset() {
359
                return charset;
1✔
360
        }
361

362
        /**
363
         * キャラクタセット(デフォルトUTF-8)を設定します。
364
         *
365
         * @param charset キャラクタセット(デフォルトUTF-8)
366
         * @return 具象型のインスタンス
367
         */
368
        @SuppressWarnings("unchecked")
369
        public T setCharset(final String charset) {
370
                try {
371
                        this.charset = Charset.forName(charset);
1✔
372
                } catch (UnsupportedCharsetException ex) {
×
373
                        this.charset = StandardCharsets.UTF_8;
×
NEW
374
                        SETTING_LOG.error(
×
375
                                        "The specified character set could not be converted to {}. Set the default character set({}).",
376
                                        charset, this.charset);
377
                }
1✔
378
                return (T) this;
1✔
379
        }
380

381
        /**
382
         * 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定するを取得します。
383
         *
384
         * @return 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定する
385
         */
386
        public List<String> getCryptColumnNames() {
387
                return cryptColumnNames;
1✔
388
        }
389

390
        /**
391
         * 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定するを設定します。
392
         *
393
         * @param cryptColumnNames 暗号化、復号化を行うカラム名のリスト. カラム名はスネークケース(大文字)で指定する
394
         * @return 具象型のインスタンス
395
         */
396
        @SuppressWarnings("unchecked")
397
        public T setCryptColumnNames(final List<String> cryptColumnNames) {
398
                this.cryptColumnNames = cryptColumnNames;
1✔
399
                return (T) this;
1✔
400
        }
401

402
        /**
403
         * 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存されるを取得します。
404
         *
405
         * @return 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存される
406
         */
407
        protected List<String> getCryptParamKeys() {
408
                return cryptParamKeys;
1✔
409
        }
410

411
        /**
412
         * 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存されるを設定します。
413
         *
414
         * @param cryptParamKeys 暗号化、復号化を行うパラメータ名リスト. キャメルケースで保存される
415
         */
416
        protected void setCryptParamKeys(final List<String> cryptParamKeys) {
417
                this.cryptParamKeys = cryptParamKeys;
1✔
418
        }
1✔
419

420
        /**
421
         * skipを取得します。
422
         *
423
         * @return skip
424
         */
425
        public boolean isSkip() {
426
                return skip;
1✔
427
        }
428

429
        /**
430
         * skipを設定します。
431
         *
432
         * @param skip skip
433
         * @return 具象型のインスタンス
434
         */
435
        @SuppressWarnings("unchecked")
436
        public T setSkip(final boolean skip) {
437
                this.skip = skip;
1✔
438
                return (T) this;
1✔
439
        }
440

441
        /**
442
         * 暗号キーを取得します。
443
         *
444
         * @return 暗号キー
445
         */
446
        protected SecretKey getSecretKey() {
447
                return secretKey;
1✔
448
        }
449

450
        /**
451
         * IVを利用するかどうかを取得します。
452
         *
453
         * @return IVを利用するかどうか
454
         */
455
        protected boolean isUseIV() {
456
                return useIV;
1✔
457
        }
458

459
        /**
460
         * 変換の名前を取得する 標準の変換名については、Java 暗号化アーキテクチャー標準アルゴリズム名のドキュメントの Cipher のセクションを参照。 初期値は<code>AES/ECB/PKCS5Padding</code>
461
         *
462
         * @return 変換の名前
463
         */
464
        public String getTransformationType() {
465
                return transformationType;
1✔
466
        }
467

468
        /**
469
         * 変換の名前を設定する 標準の変換名については、Java 暗号化アーキテクチャー標準アルゴリズム名のドキュメントの Cipher のセクションを参照。 初期値は<code>AES/ECB/PKCS5Padding</code>
470
         *
471
         * @param transformationType 変換の名前
472
         * @return 具象型のインスタンス
473
         */
474
        @SuppressWarnings("unchecked")
475
        public T setTransformationType(final String transformationType) {
476
                this.transformationType = transformationType;
1✔
477
                return (T) this;
1✔
478
        }
479

480
}
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