RubyonRailsのActiveRecordで情報を暗号化してSQLで検索もできるようにする方法(ActiveSupport::MessageEncryptor)

こんにちは！エイチーム引越し侍の加藤です！

みなさん暗号化してますか？

ユーザの情報とかデータベースに平文で入れてるぜって方は爆速で暗号化してください。
「Rails 暗号化」で検索するとMessageEncryptorに関するQiitaの記事が多く出てくると思うんですが、
この暗号化をそのまま使うと同じ文章でも違う暗号化された文字列になってしまって、
暗号化した文字列をSQLで検索できないじゃまいか！となって困ったのでその解決策を皆さんに共有したいと思います。


[Railsの可逆暗号化方法　ActiveSupport::MessageEncryptorの使用方法](https://doruby.jp/users/road_of_beginners/entries/Rails_ActiveSupport_MessageEncryptor_)

こちらでさらーっと書いてあるので楽勝じゃん！と思って継承クラス作ってみる。

```
  1 # ivがランダムだと暗号化した値で検索できないためivを固定化する継承クラス
  2 class StaticEncryptor < ::ActiveSupport::MessageEncryptor
  3   private
  4     def _encrypt(value)
  5       cipher = new_cipher
  6       cipher.encrypt
  7       cipher.key = @secret
  8 
  9       # Rely on OpenSSL for the initialization vector
 10       # 検索の為に固定化
 11       iv = "hogehogehogehoge"  # 16bitじゃないとダメらしい
 12       
 13       cipher.auth_data = "" if aead_mode?
 14 
 15       encrypted_data = cipher.update(@serializer.dump(value))
 16       encrypted_data << cipher.final
 17 
 18       blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
 19       blob << "--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
 20       blob
 21     end
 22 end
```

これで暗号化したらできたぜ！と思うんですが、
復号時にInvalid Message!のエラーが出力されます。

なんでだー！と思って元のクラスをいろいろ見てみると

```
 94       def _encrypt(value)
 95         cipher = new_cipher
 96         cipher.encrypt
 97         cipher.key = @secret
 98 
 99         # Rely on OpenSSL for the initialization vector
100         iv = cipher.random_iv
101         cipher.auth_data = "" if aead_mode?
102 
103         encrypted_data = cipher.update(@serializer.dump(value))
104         encrypted_data << cipher.final
105 
106         blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
107         blob << "--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
108         blob
109       end
```

ん？？？
100行目固定化するのはいいんだけど、そのあとiv使われてるのって暗号化する文字列として106行目で添えているだけだな…
どこで暗号化に使われているんだろう…
と弊社のエンジニアの神様が鋭い洞察で、
100行目でcipher.random_ivが実行されたときに内部的にivを保持しているはず！と看破！

```
  1 # ivがランダムだと暗号化した値で検索できないためivを固定化する継承クラス
  2 class StaticEncryptor < ::ActiveSupport::MessageEncryptor
  3   private
  4     def _encrypt(value)
  5       cipher = new_cipher
  6       cipher.encrypt
  7       cipher.key = @secret
  8 
  9       # Rely on OpenSSL for the initialization vector
 10       # 検索の為に固定化
 11       iv = "hogehogehogehoge"  # 16bitじゃないとダメらしい
 12       cipher.iv = iv
 13       cipher.auth_data = "" if aead_mode?
 14 
 15       encrypted_data = cipher.update(@serializer.dump(value))
 16       encrypted_data << cipher.final
 17 
 18       blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
 19       blob << "--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
 20       blob
 21     end
 22 end
```

こうやって12行目でcipherにもivを反映させてあげると復号もうまくいきました。


以上です。
いかがでしたか？

同じ悩みを持っている方もいるんじゃないかなーと思って記事にしてみました。
これでみなさんにハッピーエンジニアライフを送る助力ができたらと思います。

# 追伸
株式会社エイチーム引越し侍では、一緒にサイト改善をしてくれるWebエンジニアを募集しています。エイチームグループのエンジニアとして働きたい！という方は是非、以下のリンクから応募してください。
皆様からのご応募、お待ちしております！！

# 神様からの追伸
![image.png](https://qiita-image-store.s3.amazonaws.com/0/146642/9b23fa0a-14f5-f5bb-6dfd-4a6a746774a0.png)
https://github.com/ruby/openssl/blob/master/ext/openssl/ossl_cipher.c
https://github.com/ruby/openssl/blob/master/lib/openssl/cipher.rb
https://github.com/ruby/openssl/blob/master/ext/openssl/ossl_rand.c


> [エイチームグループ採用サイト（Web開発エンジニア職）](http://hikkoshi.a-tm.co.jp/recruit/request.php?ea=ktqiita_static_encryptor)