【Kotlin/Android】Cipherの使い方!暗号化・複合化の実装方法!

この記事からわかること
- Android Studio/Kotlinで暗号化・複合化を実装する方法
- Cipherの使い方
- 公開鍵と秘密鍵を生成するには?
- 初期化ベクトルとは?
- AESやRSAの違い
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
暗号化と複合化について
データをやり取りする際に悪意を持った第三者にデータを盗聴された際にデータがそのままの形式(平文という)だと中身を見られた場合に情報が全て筒抜けになってしまいます。見られることを意図していないデータであれば「暗号化」を行うことでデータの解読を困難にすることがセキュリティ対策の基本となります。
「暗号化」とは元のデータをそのままでは理解不能な形式に変換するセキュリティ手法の1つです。特定の暗号化アルゴリズムに従い、正規の手順を踏まないと複合化(暗号文も元の形式に戻すこと)できないようにすることで、万が一盗聴されても情報の流出を避けることができます。
暗号化・複合化するためには鍵が必要になります。鍵の方式にも「公開鍵暗号方式」や「共通鍵暗号方式」などの種類があり、両者の違いは名前の通り、ざっくりいうと同じ鍵を使用するかしないかです。それに伴い「公開鍵暗号方式」は安全性が高く、「共通鍵暗号方式」は処理速度が速いのが特徴です。さらにその中に暗号化アルゴリズムで種類が分かれています。
暗号化アルゴリズムの種類
「公開鍵暗号方式」と「共通鍵暗号方式」では使用される暗号化アルゴリズムが異なります。よく使われているものだけ紹介しておきます。
RSA:公開鍵暗号方式
メジャーなアルゴリズム。暗号化と同時に電子署名を行えるのが特徴で素数による素因数分解を使用した数学的アルゴリズムを使用することで高い安全性がある。
AES:共通鍵暗号方式
「Advanced Encryption Standard」の略称。データの入替や排他的論理和の計算、行列変換などの処理を組み合わせてデータを128・192・256ビットの長さに暗号化。
ハッシュ化について
暗号化と似たようなものに「ハッシュ化」があります。これは平文を異なる形式に変換するという点では同じですが、暗号化と異なり複合化(元のデータに戻すこと)できない不可逆の性質となっています。
元のデータをハッシュ化して生成される値は元のデータが同じなら同じハッシュ値になるという特徴があります。この特徴を利用してデータの整合性を担保するための活用されます。
例えばパスワード情報をハッシュ化して保存しておくことで、万が一保存されている場所からデータが流出しても、得られるのは意味のない文字の羅列のみでパスワード自体の流出を防ぐことができます。同じパスワードからは同じハッシュ値が生成されるのでパスワードの確認に影響もありません。
ハッシュ規格
元のデータをハッシュ化するためにはハッシュ関数と呼ばれるアルゴリズムを使用します。規格がいくつかあるのでまとめておきます。
MD5
メジャーなハッシュ関数。128ビットの長さのハッシュ値を高速に出力できる
SHA-1
160ビットのハッシュ値を生成する。現在は利用が推奨されていない。
SHA-2
SHA-1を改良した規格。ハッシュ値の長さに応じて、SHA-224、SHA-256、SHA-384、SHA-512が定義。
CRC32
データに対して巡回冗長検査を行い固定長(32ビット)のハッシュ値を生成する規格。
Cipherの使い方
Cipher
はAndroidでデータの暗号化やハッシュ関連の機能を提供するクラスです。
実装する流れ
- 暗号化アルゴリズムを指定してCipherオブジェクトの生成
- 暗号/復号化モード、秘密鍵/公開鍵、初期化ベクトルを渡して初期化
- doFinalメソッドで暗号/復号化
他にはGoogleが開発している暗号化ライブラリTinkなどもあります。
1.暗号化アルゴリズムを指定してCipherオブジェクトの生成
暗号化や複合化を実装するためには最初にCipherオブジェクトを生成する必要があります。インスタンス化する際にはgetInstance
メソッドを使用し、引数には暗号/複合化する暗号化アルゴリズムとパディングスキームを[algorithm/mode/padding]形式または[algorithm]形式で指定します。今回は例としてAES(共通鍵暗号方式)で暗号化していきます。
// algorithm/mode/padding 形式
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// algorithm 形式
val cipher = Cipher.getInstance("RSA");
2.暗号/復号化モード、秘密鍵/公開鍵、初期化ベクトルを渡して初期化
続いて生成したCipherオブジェクトを暗号/復号化モード、秘密鍵/公開鍵、初期化ベクトルなど渡して初期化します。初期化するためには秘密鍵または公開鍵が必要になり、今回はAES(共通鍵暗号方式)なのでSecretKeySpec
クラスを使用して秘密鍵を生成しておきます。
val secretKey = SecretKeySpec("1234567890123456".toByteArray(), "AES")
val iv = SecretKeySpec("1234567890123456".toByteArray(), "AES")
両方準備できたらinit
メソッドを使用してCipherオブジェクト初期化します。暗号化用のCipherオブジェクトの場合はENCRYPT_MODE
、複合化用のCipherオブジェクトの場合はDECRYPT_MODE
を指定します。
// 暗号化用
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv)
// 複合化用
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv)
2.1「鍵」と「初期化ベクトル(IV)」
今回作成した「秘密鍵」は暗号化・複合化時に実際に利用する「共通鍵」です。共通の鍵を利用するため、管理方法には注意が必要です。
「初期化ベクトル(Initialization Vector)」とは同じ文字列から同じ暗号化文が生まれないようにするためのビット列のことを指します。ハッシュ生成の際にも同じような目的でソルト(文字の前につける文字列)が利用されますがソルトは不可逆なのに対し、IVは複合化できるのが大きな違いです。
なのでここで指定する「鍵」と「初期化ベクトル」の文字列はなんでも良いかと思います。
3.doFinalメソッドで暗号/復号化
ここまでのCipher実装の処理を1つのユーティリティークラスとして切り出してみました。暗号化するためのencrypt
メソッドと復号化するためのdecrypt
を新しく実装しています。
暗号化も複合化もdoFinal
メソッドを使用して実装します。引数には暗号/複合化対象の文字列をByteArray
型で渡し、返り値として暗号/複合化された文字列がByteArray
型で取得できます。
class CipherUtility {
companion object {
private const val AES_ALGORITHM = "AES/CBC/PKCS5Padding"
}
private val secretKey = SecretKeySpec("1234567890123456".toByteArray(), "AES")
private val iv = IvParameterSpec("1234567890123456".toByteArray())
// 暗号化メソッド
public fun encrypt(text: String): String {
val byteText = text.toByteArray()
val cipher = Cipher.getInstance(AES_ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv)
// ここで暗号化
val encryptedBytes: ByteArray = cipher.doFinal(byteText)
return Base64.getEncoder().encodeToString(encryptedBytes)
}
// 複合化メソッド
public fun decrypt(encryptedText: String): String {
val encryptedBytes = Base64.getDecoder().decode(encryptedText)
val cipher = Cipher.getInstance(AES_ALGORITHM)
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv)
// ここで複合化
val decryptedBytes: ByteArray = cipher.doFinal(encryptedBytes)
return String(decryptedBytes)
}
}
ByteArray
(バイナリデータ)として取得した文字列はString型に変換するためにBase64
を使用します。エンコード/デコードするためにはgetEncoder
/getDecoder
メソッドからencodeToString
/decode
メソッドを呼び出し引数に対象のByteArray
を渡すだけです。
// エンコード
Base64.getEncoder().encodeToString(encryptedBytes)
// デコード
Base64.getDecoder().decode(encryptedText)
3.1Base64とは?
Base64とはバイナリデータをテキスト形式に変換するためのエンコーディング手法の1つです。A-Z
、a-z
、0-9
、+/
の64種類で文字を表します。
(※正確には=
がパディング(余った部分を詰める用)として使用されるので65種類)
例えばHello World
を先ほどの共通鍵と初期化ベクトルで暗号化してBase64の文字列に変換すると以下の様になります。
ZyODokM33Io1ZKIA8h7owA==
この文字列を先ほど定義したdecrypt
メソッドで複合化すれば正常にHello World
を取得することが可能です。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val cipherUtility = CipherUtility()
val text = cipherUtility.encrypt("Hello World")
Log.e("---", text)
val text2= cipherUtility.decrypt(text)
Log.e("---", text2)
}
}
RSAで暗号化する
RSA
で暗号化する際は公開鍵と秘密鍵の2つを生成する必要があるのでKeyPairGenerator
クラスを使用します。
class CipherRSAUtility {
companion object {
private const val RSA_ALGORITHM = "RSA"
private const val RSA_KEY_SIZE = 2048
fun generateKeyPair(): Pair<ByteArray, ByteArray> {
val keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM)
keyPairGenerator.initialize(RSA_KEY_SIZE)
val keyPair = keyPairGenerator.generateKeyPair()
val publicKey = keyPair.public.encoded
val privateKey = keyPair.private.encoded
return Pair(publicKey, privateKey)
}
fun encrypt(text: String, publicKeyBytes: ByteArray): String {
val publicKey = KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(X509EncodedKeySpec(publicKeyBytes))
val cipher = Cipher.getInstance(RSA_ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray())
return Base64.getEncoder().encodeToString(encryptedBytes)
}
fun decrypt(encryptedText: String, privateKeyBytes: ByteArray): String {
val encryptedBytes = Base64.getDecoder().decode(encryptedText)
val privateKey = KeyFactory.getInstance(RSA_ALGORITHM).generatePrivate(PKCS8EncodedKeySpec(privateKeyBytes))
val cipher = Cipher.getInstance(RSA_ALGORITHM)
cipher.init(Cipher.DECRYPT_MODE, privateKey)
val decryptedBytes = cipher.doFinal(encryptedBytes)
return String(decryptedBytes)
}
}
}
使用する際はこんな感じ。
val keyPair = CipherRSAUtility.generateKeyPair()
val publicKey = keyPair.first
val privateKey = keyPair.second
val text = CipherRSAUtility.encrypt("Hello, World!", publicKey)
Log.e("---", text)
val text2= CipherRSAUtility.decrypt(text, privateKey)
Log.e("---", text2)
暗号化アルゴリズムとパディングスキームの種類
AES/CBC/NoPadding
(128)AES/CBC/PKCS5Padding
(128)AES/ECB/NoPadding
(128)AES/ECB/PKCS5Padding
(128)DES/CBC/NoPadding
(56)DES/CBC/PKCS5Padding
(56)DES/ECB/NoPadding
(56)DES/ECB/PKCS5Padding
(56)DESede/CBC/NoPadding
(168)DESede/CBC/PKCS5Padding
(168)DESede/ECB/NoPadding
(168)DESede/ECB/PKCS5Padding
(168)RSA/ECB/PKCS1Padding
(1024、2048)RSA/ECB/OAEPWithSHA-1AndMGF1Padding
(1024、2048)RSA/ECB/OAEPWithSHA-256AndMGF1Padding
(1024、2048)
参考文献:https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Cipher.html
鍵をセキュアに保管する
暗号化に使用する鍵はセキュアに保管しておくことが大事です。そのために仕組みとしてKeyStoreがあるので利用してみてください。
Swiftで暗号化・複合化を実装する
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。