SpringBootで暗号化・復号処理の実装
はじめに
DBにデータを保存する際に、セキュリティ向上のため暗号化してDBに保存することにした。
目次
参考サイト
実装
設定値の追加
application.ymlに暗号化に使用するキーの設定値を追加した
my_constants: security: key: (英数大文字小文字・数字・記号を使った16文字)
キーは16文字で、正直決めるのが面倒くさかったので、以下のサイトでランダム生成した
Beanの追加
暗号化・復号処理で使うインスタンスをBeanで用意する
@Configuration public class CommonConfig { @Bean public SecretKeySpec secretKeySpec(@Value("${my-constants.security.key}") String key) { return new SecretKeySpec(key.getBytes(), "AES"); } @Bean public Cipher cipher() throws NoSuchPaddingException, NoSuchAlgorithmException { return Cipher.getInstance("AES/CBC/PKCS5Padding"); } }
ログ出力
ログ出力にデフォルト実装を使った
/** * ログを出力する機能を実装する * * ログを出力したいクラスで、このインターフェースを実装する<br/> * * <pre> * class Foo implements Loggable { * public void bar() { * logger().debug("Hello world!!!"); * } * } * </pre> */ public interface Loggable { /** * デフォルト実装 * * @return ロガー */ default Logger logger() { return LoggerFactory.getLogger(this.getClass()); } }
暗号化・復号処理クラスの作成
実際の暗号化・復号化処理は以下の通り
@Component public class Crypt implements Loggable { private final SecretKeySpec secretKeySpec; private final Cipher cipher; // 暗号化・復号処理が同時に呼び出されたときに、Cipher#initを実行しないようにするためのロック // 実際に同時にCipher#initした時にどうなるかは未確認。良くないことが起きそう気がするのでロックしている // cipherを暗号化用/復号用でインスタンスを分けるのもアリかも private final Object lockObj = new Object(); @Autowired public Crypt(SecretKeySpec secretKeySpec, Cipher cipher) { this.secretKeySpec = secretKeySpec; this.cipher = cipher; } /** * 暗号化する * * @param str 暗号化する文字列 * @return 暗号化した文字列 */ public String encrypt(String str) { synchronized (this.lockObj) { this.setEncryptMode(); byte[] encodedBytes; try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { // 最初に初期ベクトルを出力 outputStream.write(this.cipher.getIV()); // ByteArrayOutputStream#toByteArray実行前に、CipherOutputStreamを先にcloseしないと復号化に失敗する try (CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, this.cipher)) { cipherOutputStream.write(str.getBytes()); } encodedBytes = outputStream.toByteArray(); } catch (IOException e) { this.logger().error("failed cipher."); throw new CryptException(e); } byte[] base64Bytes = Base64.getEncoder().encode(encodedBytes); return new String(base64Bytes); } } /** * 復号する * * @param str 復号する文字列 * @return 復号した文字列 */ public String decrypt(String str) { synchronized (this.lockObj) { byte[] base64Bytes = Base64.getDecoder().decode(str); try (ByteArrayInputStream inputStream = new ByteArrayInputStream(base64Bytes)) { byte[] iv = getIVForDecode(inputStream); this.setDecryptMode(iv); return this.decryptCipherStream(inputStream); } catch (IOException e) { this.logger().error("failed decode."); throw new CryptException(e); } } } /** * 暗号化モードに設定する */ private void setEncryptMode() { try { this.cipher.init(Cipher.ENCRYPT_MODE, this.secretKeySpec); } catch (InvalidKeyException e) { this.logger().error("failed initialize cipher."); throw new CryptException(e); } } /** * 復号モードに設定する */ private void setDecryptMode(byte[] iv) { IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); try { this.cipher.init(Cipher.DECRYPT_MODE, this.secretKeySpec, ivParameterSpec); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { this.logger().error("failed initialize cipher."); throw new CryptException(e); } } /** * 暗号化されたストリームを復号する * * @param inputStream ストリーム * @return 復号されたデータ */ private String decryptCipherStream(ByteArrayInputStream inputStream) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { try (CipherInputStream cipherInputStream = new CipherInputStream(inputStream, this.cipher)) { byte[] buffer = new byte[1024]; int length; while ((length = cipherInputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, length); } } return outputStream.toString(); } catch (IOException e) { this.logger().error("failed decode."); throw new CryptException(e); } } /** * 暗号化されたデータから初期ベクトルを取得する。 * * @param inputStream 暗号化データのストリーム * @return 初期ベクトル */ private static byte[] getIVForDecode(ByteArrayInputStream inputStream) { byte[] iv = new byte[16]; for (int i = 0; i < iv.length; ++i) { iv[i] = (byte)inputStream.read(); } return iv; } }
例外クラス
暗号化・復号失敗時の例外クラス
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) public class CryptException extends RuntimeException { public CryptException() { } public CryptException(String message) { super(message); } public CryptException(String message, Throwable cause) { super(message, cause); } public CryptException(Throwable cause) { super(cause); } public CryptException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
テスト
適当にテストコードを書いてみた。
10万回ほどランダムな文字列を作成して、暗号化→復号して文字列が一致するかテスト
何回か実行して大丈夫だった。
@RunWith(SpringRunner.class) @SpringBootTest public class CryptTests implements Loggable { @Autowired private Crypt testTarget; @Test public void cryptTest() { IntStream.rangeClosed(1, 100_000) .forEach(i -> { int randomNumber = Integer.parseInt(RandomStringUtils.randomNumeric(1, 5)); String rawText = RandomStringUtils.randomAlphanumeric(randomNumber); this.logger().info(String.format("rawText text:%s", rawText)); String encodedText = this.testTarget.encrypt(rawText); this.logger().info(String.format("encoded text:%s", encodedText)); String decodedText = this.testTarget.decrypt(encodedText); assertEquals(rawText, decodedText); }); } }
おわりに
取り敢えず暗号化する処理はできた。
暗号化するとデータが増加するので、暗号化前のデータを圧縮した方が良いと何処かで見かけたので、ZipOutputStreamとかGZIPOutputStreamを使って見たけど、上手く行かなかった・・・