読者です 読者をやめる 読者になる 読者になる

Got Some \W+ech?

Could be Japanese. Could be English. Android, セキュリティ, 機械学習などをメインに、たまにポエムったり雑感記載したりします。

AndroidのConcealライブラリの中身を読む

色々あれがこれなので、Facebookが出している暗号化ライブラリ「Conceal」のソースコードを読んでみました。

  • 使い方については、こちらの記事を参照いただくといいと思います。

Concealとは

Concealライブラリの中身

Concealのgithubページ・上記Qiita記事に載っているので実装は割愛しますが、大きく4ステップがあります。

  1. KeyChainインスタンスの生成
  2. Cryptoインスタンスの生成
  3. ライブラリのロード状況チェック
  4. 暗号化

それでは各ステップでConcealが何をしているか読み込んでいきましょう。

KeyChainインスタンスの生成

KeyChainは共通鍵や関係するデータのライフサイクルに関わるメソッド群を定義したインターフェースです。これを実装したSharedPrefsBackedKeyChainインスタンスがまず生成されます。ファイル名の通り、SharedPrefをベースにしていますが、暗号化に欠かせない疑似乱数生成器(PRNG)の生成と、暗号化時の鍵長を設定します。

PRNGの生成 と 既知の脆弱性対応

  • 該当コード
# SharedPrefesBackedKeyChain.java
SecureRandomFix.createLocalSecureRandom()

PRNGはOpenSSLを利用して生成されますが、Jelly Bean(16~18)には初期化プロセスに脆弱性がありました。 この脆弱性をつくことで暗号文の強度が低下し、BitCoinアプリ内のウォレットの盗難などができてしまう模様です。Googleはこれへの対処方をDeveloper Blogに公開していますが、これをConcealがカバーしてくれています。

private static void tryApplyOpenSSLFix() {
  try {
    // Mix in the device- and invocation-specific seed.
    Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
        .getMethod("RAND_seed", byte[].class)
        .invoke(null, generateSeed());
    // Mix output of Linux PRNG into OpenSSL's PRNG
    int bytesRead = (Integer) Class.forName(
        "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
        .getMethod("RAND_load_file", String.class, long.class)
        .invoke(null, DEV_URANDOM, 1024);
    if (bytesRead != 1024) {
      throw new IOException(
          "Unexpected number of bytes read from Linux PRNG: "
              + bytesRead);
    }
  } catch (Exception e) {
    throw new SecurityException("Failed to seed OpenSSL PRNG", e);
  }
}

暗号化時の鍵長を設定

crypto\CryptoConfig.javaで、鍵長・初期化ベクトル長・タグ長の組み合わせがenumで定義されています。とはいえ、その組み合わせは2通りしかなく、しかも鍵長にしか違いはありません。2016年4月にリリースされたv1.1から、デフォルト推奨の鍵長が256bitになりましたので、基本はCryptoConfig.KEY_256を使いましょう。

  • デフォルトenum:
    • cipherID: 2
    • 鍵長: 256bits
    • 初期化ベクトル長: 12bits
    • タグ長: 16bits

Cryptoインスタンスの生成

前述のKeyChainをセットしAndoidConceal().get().createDefaultCrypto(keyChain)で、生成しています。AndroidConceal().get()内でまた SecureRandomFix.createLocalSecureRandom()を呼び出しています。おそらくこれは旧バージョンと新バージョンでの使用方法の差異に発する実装かと思われます。また、Cryptoインスタンス生成時に暗号化アルゴリズム(AES-GCM)が決定されます。今後、これが拡張されて他のアルゴリズムや暗号利用モードが利用できるようになる...ことはないでしょう。多分。

ライブラリのロード状況チェック

ConcealのAESアルゴリズム・GCMはCで書かれているため、Conceal内でNativeライブラリ"crypto"がロードされます。このチェックをcrypto.isAvailable()がしています

暗号化

Concealで暗号化は、平文を書き込む出力ストリームをラップすることで実現されます。デフォルトの実装では、Cryptoインスタンスを通じてBufferedOutputSreamをラップし、そのストリームに平文を書き込む形になります。

出力ストリームのラップ

実際のラップは、Cryptoインスタンス内のmCryptoAlgo変数が担当します。

# Crypto.java
    public OutputStream getCipherOutputStream(OutputStream cipherStream, Entity entity, byte[] encryptBuffer)
      throws IOException, CryptoInitializationException, KeyChainException {
        return mCryptoAlgo.wrap(cipherStream, entity, encryptBuffer);
    }

wrap内では、KeyChain内のPRNGから初期化ベクトルが生成され、他のメタデータと一緒に、ラップされる前の出力ストリームに書き込まれます。また、共有鍵もこのタイミングで生成されます。KeyChainのgetCipherKey()は、 maybeGenerateKey()を呼び出し、鍵を生成(もしくは取得)し、SharedPreferenceに保存します。

# CryptoAlgoGcm.java
    @Override
    public OutputStream wrap(OutputStream cipherStream, Entity entity, byte[] buffer)
            throws IOException, CryptoInitializationException, KeyChainException {
        cipherStream.write(VersionCodes.CIPHER_SERIALIZATION_VERSION);
        cipherStream.write(mConfig.cipherId);

        byte[] iv = mKeyChain.getNewIV();
        NativeGCMCipher gcmCipher = new NativeGCMCipher(mNativeLibrary);
        gcmCipher.encryptInit(mKeyChain.getCipherKey(), iv);
        cipherStream.write(iv);
    }
    
# SharedPrefsBackedKeyChain.java
  @Override
  public synchronized byte[] getCipherKey() throws KeyChainException {
    if (!mSetCipherKey) {
      mCipherKey = maybeGenerateKey(CIPHER_KEY_PREF, mCryptoConfig.keyLength);
    }
    mSetCipherKey = true;
    return mCipherKey;
  }
  
    private byte[] generateAndSaveKey(String pref, int length) throws KeyChainException {
    byte[] key = new byte[length];
    mSecureRandom.nextBytes(key);
    // Store the session key.
    SharedPreferences.Editor editor = mSharedPreferences.edit();
    editor.putString(
        pref,
        encodeForPrefs(key));
    editor.commit();
    return key;
  }

その後にGCM特有のデータと一緒に、AES-GCMで入力データを暗号化されるNativeGCMCipherOutputStreamが生成されます。

# CryptoAlgoGcm.java
    @Override
    public OutputStream wrap(OutputStream cipherStream, Entity entity, byte[] buffer)
            throws IOException, CryptoInitializationException, KeyChainException {
        byte[] entityBytes = entity.getBytes();
        computeCipherAad(gcmCipher, VersionCodes.CIPHER_SERIALIZATION_VERSION, mConfig.cipherId, entityBytes);
        return new NativeGCMCipherOutputStream(cipherStream, gcmCipher, buffer, mConfig.tagLength);
    }

暗号化

上記で生成したNativeGCMCipherOutputStreamに平文がwriteされると、NativeGCMCipher.updateが呼ばれ、gcm.c内のJava_com_facebook_crypto_cipher_NativeGCMCipher_nativeUpdateAadで暗号化されるのかと思われます。AndroidのNDKの仕組みに詳しくないので、間違っていたら教えて下さい。

# NativeGCMCipherOutputStream.java

@Override
  public void write(byte[] buffer, int offset, int count)
      throws IOException {
    if (buffer.length < offset + count) {
      throw new ArrayIndexOutOfBoundsException(offset + count);
    }

    int times = count / mUpdateBufferChunkSize;
    int remainder = count % mUpdateBufferChunkSize;

    for (int i = 0; i < times; ++i) {
      int written = mCipher.update(buffer, offset, mUpdateBufferChunkSize, mUpdateBuffer, 0);
      mCipherDelegate.write(mUpdateBuffer, 0, written);
      offset += mUpdateBufferChunkSize;
    }

    if (remainder > 0) {
      int written = mCipher.update(buffer, offset, remainder, mUpdateBuffer, 0);
      mCipherDelegate.write(mUpdateBuffer, 0, written);
    }
  }

以上が、一般的なConcealの暗号化が行われるステップになります。

Conceal - 他の機能

より単純な暗号化の実装

Cryptoインスタンスの生成までは同じですが、そのあとはただ一行「encrypt()」でおわる実装方法です。もう片方との違いは、出力ストリームが受け取れるバイト長が固定であることです。一回生成したら変更がないイミュータブルなデータ、もしくはハッシュ化したデータを暗号化したい時に使えるかもしれません。

KeyChain keyChain = new SharedPrefsBackedKeyChain(context,CryptoConfig.KEY_256);
Crypto crypto = AndroidConceal.get().createDefaultCrypto(keyChain);
crypto.encrypt(plainTextInByte, Entity.create("unique_id"))

パスワードベースの鍵の生成

Concealは基本的に鍵の生成・初期化ベクトルの作成など、暗号化で必要な処理は全てやってくれます。もし、そういった利便性を投げ捨ててオリジナルな鍵を作りたいのであれば、パスワードから生成することができます。しかし、あくまでも鍵を生成するところまでで、そのあとの暗号化処理、初期化ベクトルの作成、鍵の保存を全て自前で実装する必要があります。

AndroidConceal.get().createPasswordBasedKeyDerivation()
     .setIterations(10000)
     .setPassword("P4$$word")
     .setSalt(buffer)
     .setKeyLengthInBytes(16) // in bytes
     .generate();

MAC値の取得

Concealを利用すれば、デフォルトの暗号利用モードがGCMになってます。GCMは認証付きアルゴリズムなので、MAC値の計算は必要ありません。しかし、パスワードベースの鍵の生成をした場合、複合時に完全性・認証をしたほうがいいので、その場合はこれをつかうといいと思います。なお、複合に完全性・認証チェックがなぜ必要なのかを知りたい場合は、オラクルパディング攻撃でggrと良いでしょう。

OutputStream fileStream = new BufferedOutputStream(new FileOutputStream(file));
OutputStream outputStream = crypto.getCipherOutputStream(fileStream,Entity.create("entity_id"));
crypto.getMacOutputStream(outputStream,Entity.create("unique_mac_id")); // probably
outputStream.write(plainTextBytes);
outputStream.close();

Concealを使うにあたって気をつけたいこと

最後にConcealを使うにあたって気をつけたいことを書きましょう。

  • 上述しましたが、ver1.1から256bit鍵長が推奨されています。記事の中にはver1.1以前のもあるので気をつけましょう。
  • 鍵をSharedPreferenceに保存しているので、Root化された端末では抜かれてしまいます。
  • API18以降を対象とするならおとなしくKeyStoreに保存しましょう。その場合ユーザーが入力するパスワードがなければ開けない
  • というより、大事な情報は基本サーバに起き、Android端末には保存しないようにしましょう。必要な時にだけfetchしに行きましょう。
  • もし、サーバにおけない場合は...いい案があったら教えてください。Root化された端末では利用できないようにする...とか?

参考

GoでAESアルゴリズム(GCMモード)を使った実装をする

GoでAESアルゴリズム(CBCモード)+PKCS7パディング+HMACを使った実装をするの続きです

さて、前回はHMACを組み合わせることで、暗号化における機密性(Confidentiality)だけでなく、完全性(Integrity)からなる認証まで実現することができました。しかし、これには1つ問題があります。面倒くさい! 面倒くさければ、手間を省こうと実装しないかもしれないし、よしんばしても実装ミスが発生するおそれがあります。その問題に対応するために出てきたのが、認証付き暗号(Authentication Encryption)です。

GCMはその1つで、アメリカ国立標準技術研究所(NIST)にも標準として認められています。また、暗号処理を並列化することで高速な処理を可能としています。そのためなのか、最新ブラウザ <--> サーバ間通信であれば、デフォルトでAES-GCMのTLS通信として選択されています。

image

また、GCMはパディングが不要なストリーム型の暗号です。

GoにおけるAES-GMCの実装

なんと、Goの"crypto/cipher"パッケージにデフォルトで入ってました。hmacみたいに独立してないし、Paddingのように自前実装が必要ないので、暗に「お前ら、うだうだ言わずGCM使っとけ?」と言われている気がします。勿論、私はそうさせて頂きますよ、Google様。

// GCM encryption
func EncryptByGCM(key []byte, plainText string) ([]byte, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(block); if err != nil {
        return nil, err
    }

    nonce := make([]byte, gcm.NonceSize())// Unique nonce is required(NonceSize 12byte)
    _, err = rand.Read(nonce); if err != nil {
        return nil, err
    }

    cipherText := gcm.Seal(nil, nonce, []byte(plainText), nil)
    cipherText = append(nonce, cipherText...)

    return cipherText, nil
}

// Decrypt by GCM
func DecryptByGCM(key []byte, cipherText []byte) (string, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return "", err
    }
    gcm, err := cipher.NewGCM(block); if err != nil {
        return "", err
    }

    nonce := cipherText[:gcm.NonceSize()]
    plainByte, err := gcm.Open(nil, nonce, cipherText[gcm.NonceSize():], nil); if err != nil {
        return "", err
    }

    return string(plainByte), nil
}

func main() {
    cipherText, _ = EncryptByGCM(key, "12345")
    decryptedText, _ = DecryptByGCM(key, cipherText)
    fmt.Printf("Decrypted Text: %v\n ", decryptedText)
        // Decrypted Text: 12345
}

以上。取り急ぎ、これでGoにおけるAESアルゴリズムを用いた実装は一通り紹介出来たかと思います。

NONCE, 初期化ベクトルについて

これらは各データで一度きりしか使われないことを担保しなければいけません。元々の存在理由がリプレイ攻撃への対策です。 リプレイ攻撃は、例えばEC2サイトに注文した暗号通信を盗聴し、再度おなじ暗号通信を再送(リプレイ)することで2重の発注をさせてしまう様な攻撃です。もしくは、自分の口座に対する送金をする通信を盗聴し、それをリプレイすることで、何回も振り込ませてしまうような攻撃です。

これを使い捨てのナンスを使い暗号文を検証することで防御できるので、使いまわしてはいけません。なので、毎回ランダムに作り直しましょう。

GoでAESアルゴリズム(CBCモード)+PKCS7パディング+HMACを使った実装をする

GoでAESアルゴリズム(CBCモード)+PKCS7パディング+HMACを使った実装をするの続きです。

さて、前回はAES+CBC+PKCSパディングを使った実装例を紹介した。パディングを使って、任意の平文をブロック型暗号で暗号化することが可能になったが、一方で脆弱性がうまれてしまった。暗号文+パディングを繰り返し送ることで平文を一部推測できてしまうような脆弱性だ。この脆弱性の根本原因は、誰もが復号できてしまう部分にある。従って、復号処理のための暗号文を入力するものを認証することで解決できる。その仕組がMAC(Message Authentication Code)であり、今回はHMAC(keyed-hash MAC)を使った実装例が今回のものになる。

AES暗号化によるHMACとは

暗号文をHMAC用の共有鍵とハッシュ関数で導きだされるもの。復号をするまえにHMACを検証することで、共有鍵を持っているサブジェクトを認証することができる。

GoにおけるHMACは?

標準パッケージ内のcrypt/hmacで実装されている。パディングは実装されていなかったのに、なぜなのか。謎である。実装自体はシンプルになる。

func EncryptByCBCMode(key []byte, plainText string) ([]byte, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    paddedPlaintext := PadByPkcs7([]byte(plainText))
    cipherText := make([]byte, len(paddedPlaintext)) // cipher text must be larger than plaintext
    iv := make([]byte, aes.BlockSize)// Unique iv is required
    _, err = rand.Read(iv); if err != nil {
        return nil, err
    }

    cbc := cipher.NewCBCEncrypter(block, iv)
    cbc.CryptBlocks(cipherText, paddedPlaintext)
    cipherText = append(iv, cipherText...)

        // MAC作成
    mac := hmac.New(sha256.New, []byte("12345678912345678912345678912345")) // sha256のhmac_key(32 byte)
    mac.Write(cipherText)
    cipherText = mac.Sum(cipherText)
    return []byte(cipherText), nil
func DecryptByCBCMode(key []byte, cipherText []byte) (string, error) {
    if len(cipherText) < aes.BlockSize + sha256.Size {
        panic("cipher text must be longer than blocksize")
    } else if len(cipherText) % aes.BlockSize != 0 {
        panic("cipher text must be multiple of blocksize(128bit)")
    }

    // macの取り出し
    macSize := len(cipherText) - sha256.Size
    macMessage := cipherText[macSize:]

        // 暗号文から想定macを計算
    mac := hmac.New(sha256.New, []byte("12345678912345678912345678912345")) // sha256のhmac_key(32 byte)
    mac.Write(cipherText[:macSize])
    expectedMAC := mac.Sum(nil)

        // MACによる認証
    if !hmac.Equal(macMessage, expectedMAC) {
        return "", errors.New("Failed Decrypting")
    }

    iv := cipherText[:aes.BlockSize]
    plainText := make([]byte, len(cipherText[aes.BlockSize:macSize]))
    block, err := aes.NewCipher(key); if err != nil {
        return "", err
    }
    cbc := cipher.NewCBCDecrypter(block, iv)
    cbc.CryptBlocks(plainText, cipherText[aes.BlockSize:macSize])

    return string(UnPadByPkcs7(plainText)), nil
}

その他

AES +CBC +パディング + hmacでためしたが、 AES +GCMで全てやりたかったことが出来るみたいなので、次回はそちらを試してみる.

GoでAESアルゴリズム(CBCモード)+PKCS7パディングを使った実装をする 編集

GoでAESアルゴリズム(CBCモード)を使った実装をするの続き

AESはブロック暗号であるため、16の倍数バイトの平文にしか適用できない。そうでない平文をAESで暗号化するには追加のデータ(パディング)を付与して16の倍数バイトにしてやる必要がある。AESを含めた共通鍵方式のパディングにはいくつか種類があって、Wikiによると以下の通りである。 * Bit Padding, Byte Padding, ANSI X.923, ISO 10126, PKCS#7, ISO/IEC 7816-4, Zero padding

白状すると、各々が何に適しているかはわからないし、調べる気力もなかった。ただPKCS #7は、RSA公開鍵方式で使われていたはずなので、信頼と実績でそれを使ってみようと思う。ただ、Goの場合は、Padding関係のパッケージが容易されてないので、自前実装することになる。メンドクサイ

PKCS7による実装

PKCS7のパディングは、RFC5652にある通り、追加したいパディング長を値にしたバイトを追加する形になる。数式にすると、以下のとおり。

  • k-(lth mod k)

kはブロックサイズであり、AESの場合は16になる。lthは平文(バイト)の長さになる。上記で導きだしたものが、パディングの値になる。例えば(lth mod k)=1だとしたら、01がパディングになり、(lth mod k)=2だとしたら、02 02がパディングになる。ただ、(lth mod k)=0の場合、16 16.....16がパディングになる。これをGoで実装すると以下の通り。

func PadByPkcs7(data []byte) []byte {
    padSize := aes.BlockSize
    if len(data) % aes.BlockSize != 0 {
        padSize = aes.BlockSize - (len(data)) % aes.BlockSize
    }

    pad := bytes.Repeat([]byte{byte(padSize)}, padSize)
    return append(data, pad...) // Dots represent it unpack Slice(pad) into individual bytes
}

func UnPadByPkcs7(data []byte) []byte {
    padSize := int(data[len(data) - 1])
    return data[:len(data) - padSize]
}

func EncryptByCBCMode(key []byte, plainText string) ([]byte, error) {
    //if len(plainText) % aes.BlockSize != 0 { <-いらなくなった
    //  panic("Plain text must be multiple of 128bit")
    //}

    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    paddedPlaintext := PadByPkcs7([]byte(plainText))
    fmt.Printf("Padded Plain Text in byte format: %v\n", paddedPlaintext)
    cipherText := make([]byte, aes.BlockSize + len(paddedPlaintext)) // cipher text must be larger than plaintext
    iv := cipherText[:aes.BlockSize] // Unique iv is required
    _, err = rand.Read(iv); if err != nil {
        return nil, err
    }

    cbc := cipher.NewCBCEncrypter(block, iv)
    cbc.CryptBlocks(cipherText[aes.BlockSize:], paddedPlaintext)
    cipherTextBase64 := base64.StdEncoding.EncodeToString(cipherText)
    return []byte(cipherTextBase64), nil
}

func DecryptByCBCMode(key []byte, cipherTextBase64 []byte) (string, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return "", err
    }

    cipherText, _ := base64.StdEncoding.DecodeString(string(cipherTextBase64))

    if len(cipherText) < aes.BlockSize {
        panic("cipher text must be longer than blocksize")
    } else if len(cipherText) % aes.BlockSize != 0 {
        panic("cipher text must be multiple of blocksize(128bit)")
    }
    iv := cipherText[:aes.BlockSize] // assuming iv is stored in the first block of ciphertext
    cipherText = cipherText[aes.BlockSize:]
    plainText := make([]byte, len(cipherText))

    cbc := cipher.NewCBCDecrypter(block, iv)
    cbc.CryptBlocks(plainText, cipherText)
    return string(UnPadByPkcs7(plainText)), nil
}
func main() {
    plainText = "12345" // Paddingしないとエラーになる平文
    cipherText, _ = EncryptByCBCMode(key, plainText)
    fmt.Printf("Plaintext %v is encrypted into %v:\n", plainText, cipherText)
    decryptedText,_ = DecryptByCBCMode(key, cipherText)
    fmt.Printf("Decrypted Text: %v\n ", decryptedText)
}
>> 
                                                  |-> こっからパディング
Padded Plain Text in byte format: [49 50 51 52 53 11 11 11 11 11 11 11 11 11 11 11]
Plaintext 12345 is encrypted into [116 51 109 107 87 53 103 43 86 65 114 73 104 99 75 97 85 76 121 71 109 90 122 70 83 122 90 120 98 102 112 72 43 74 111 69 48 76 79 97 98 69 56 61]:
Decrypted Text: 12345

以上。

おまけ - パディングに対する攻撃

暗号文+パディングを繰り返し送ることで平文を一部推測できてしまう攻撃手法(パディングオラクル攻撃)がある。SSL3.0における脆弱性(POODLE)はこれを利用したものになる。この攻撃を防ぐためには、復号しようとしている暗号文が正しいサブジェクトによって生成されたものかを認証ればよい(MAC)。MACは又今後書くつもり。

GoでAESアルゴリズム(CBCモード)を使った実装をする

前回ではAESアルゴリズムを用いた暗号化・復号をGoで実装してみたの続きです。

今日の内容

ただAESアルゴリズムによる暗号のみだと、攻撃者は暗号文を操作 => 平文を操作することができます。これを解決するための1つのやり方が、SSL/TLSにも用いられているCBC(Cipher Block Chaining)モードです。モードとは、固定長以上の平文をブロックにわけ、各ブロックを暗号化アルゴリズムで暗号化する手法をさします。

CBCモードとはなんぞや

CBCモードは、1つ前の暗号文ブロックと平文ブロックをXORしたものを暗号化することを繰り返す手法になります。最初の暗号文ブロックを生成する際には、ランダムに生成した初期化ベクトル(IV)を平文ブロックとXORする形になります。このIVは復号時にも利用なので別途保存する必要があり、最終的に生成される暗号文にappendして保存するのが一般的らしいです。

image

また、復号時には1つ前の暗号文ブロックと、復号された暗号文ブロックをXORしたものが平文ブロックになります。ここでもIVが必要なので、保存した暗号文からIVを取得します。

image

これにより、攻撃者による暗号文の操作したとしても目的通りの結果にさせないことが可能になります。

CBCモードにおける暗号文ブロックの変化に対する動き

  • 破損時(暗号文ブロックの値が何らかの理由により変わった場合): 破損した暗号文ブロックにより復号される平文ブロックと、その後に復号される平文ブロックに影響します。
  • ビット欠落: 欠落した暗号文ブロックにより復号される平文ブロックとその後の平文ブロックのすべて

GoによるAES CBCモードの実装

ではGoでの実装方法を見てみましょう.

func EncryptByCBCMode(key []byte, plainText string) ([]byte, error) {
    if len(plainText) % aes.BlockSize != 0 {
        panic("Plain text must be multiple of 128bit")
    }

    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    cipherText := make([]byte,  aes.BlockSize + len(plainText)) // 初期化ベクトルを保存するためにaes.BlockSizeを加えている
    iv := cipherText[:aes.BlockSize] // Unique iv is required
    _, err = rand.Read(iv); if err != nil {
        return nil, err
    }

    cbc := cipher.NewCBCEncrypter(block, iv)
    cbc.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))

    return cipherText, nil
}
func DecryptByCBCMode(key []byte, cipherText []byte) (string ,error) {
    block , err := aes.NewCipher(key); if err != nil {
        return "", err
    }

    if len(cipherText) < aes.BlockSize {
        panic("cipher text must be longer than blocksize")
    } else if len(cipherText) % aes.BlockSize != 0 {
        panic("cipher text must be multiple of blocksize(128bit)")
    }
    iv := cipherText[:aes.BlockSize] // assuming iv is stored in the first block of ciphertext
    cipherText = cipherText[aes.BlockSize:]
    plainText := make([]byte, len(cipherText))

    cbc := cipher.NewCBCDecrypter(block, iv)
    cbc.CryptBlocks(plainText, cipherText)
    fmt.Println(plainText)
    return string(plainText), nil
}
func main() {
    cipherText, _ = EncryptByCBCMode(key, "1234567891234567") // 16bye
    fmt.Println(cipherText)
    cipherText, _ = EncryptByCBCMode(key, "12345678912345671234123412341234") // 32byte
    fmt.Println(cipherText)
        // iv(32 byte) + 16byte

    plainText, _ = DecryptByCBCMode(key, cipherText)
    fmt.Println(plainText)
        // 12345678912345671234123412341234
}

以上。ただ、結局16の倍数 Byteの固定長平文しか暗号化できない。ブロック暗号の仕様上、しょうがないのでが、任意の長さの平文を暗号化したいのは当然の欲求だ。その場合、(16 - 平文%16)バイト文のパディングを追加する必要がある。次回は、それをかく。

GoでAESアルゴリズムを使った実装をする (と暗号歴史を一部紹介)

実はCISSPを受験しようと思ってて、最近その勉強をしている。(結構長く勉強してるはずなんだけど、業務が忙しくて実質時間が書けられてなくて進捗だめであああああああああああああああああああああああああああああああ)。まあ、こういうこと書いてるからなのかもしれないんだけど、しかたがない(しかたなくない)

CISSP自体に暗号はドメインとして含まれていないのだが、ドメインをまたがる問題として出題されているので、結城先生の「暗号技術入門」を読書している。

せっかくなので、そのアウトプットとしてGoでAES・CBCモードのSecret Keyを実装してみることにした。 余談だが、共通鍵方式はSecret Key, Common Key, Shared Key...など色々あるようだが、CISSP(の問題)ではSecret Keyに統一されているので、本投稿もそれに合わせる。

尚、@ken5scalは暗号技術の専門家ではないので、間違いはあると思う。何か見つけた場合は是非連絡を頂きたい。

AESアルゴリズム

さて、AESアルゴリズムの実装に入る前に、その説明だけしておこう。 これは別名Rijndaelアルゴリズムともいい、DESが機械の性能向上により物理的敗北を喫したのをきっかけに、NIST(米国立標準技術研究所)によりFIPS(連邦情報処理標準規格)と認定された。これにより保管データ暗号化時の推奨アルゴリズムとして、デファクトとなる。@ken5scalの使うMBPのディスク暗号化機能「FileVault」もAESアルゴリズムを利用している。

面白いのは、このアルゴリズムがNISTによって秘密に作成・運用されているのではなく、DESに変わるSecret Keyアルゴリズムを選定するコンペで採用されたものであり、公開されていることだ。wikiにも結城先生本にも記載されてるし、NISTもがっつり公開している。*1

実は米国は、AESが選定されるまで暗号に関しては非常にクローズな立場をとっていた。1992年の時点では暗号化ソフト(含むアルゴリズム)をAuxillary(補助的?)軍需品と指定し、輸出規制の対象としていたぐらいである。具体的にはアルゴリズム・鍵長(40bit以上)で規制をかけてた*2。当然、通信の暗号化などプライバシーの強化に大活躍したが、それと同時に犯罪者によっても悪用される。従って、法執行当局のシギント的捜査が難航するからコントロールしようとするのは当然のことだろう。包丁をコンビニに置くと...みたいな話に感じそうだが、当時の背景(第2次世界対戦後と冷戦後)を考慮すると、「せやな」と思えなくもない。当局による取り組みの1例としては、1994年に「鍵預託」(key escrow)というシステムを組み込んだ「預託暗号標準(EES)」があるだろう。ホワイトハウスはこれを承認し、電話通信・コンピューター通信に個人の秘密鍵を埋め、同時に鍵のコピーを連邦当局に保存する方向に持っていこうとしてた*3。日本のマイナンバーの前身みたいなものだろうか。

でも、結局のところ、規制を米国ほどかけたくないヨーロッパや市場からの反応により、管轄が1996年に国務省(Department of State)から商務省(Department of Commerce)に移管され、それとともに軍需品の対象ではなくなった。そして、その後色々あって(知らない)、2000年のAES選定につながる。ただし、現在でも輸出規制が完全になくなったかというとそうではなく、Secrete Keyアルゴリズムは鍵長64bit超のもの且つマスマーケットになっていないものが輸出規制対象となっている。*4

なお、米国に暗号の輸入出規制があると書いたが、別に米国に限ったものではない。武器輸出を規制するワッセナー・アレンジメントに調印した国や、独自の基準をもつ中国など、様々な規制があって面白いので、別途調べてみたい。*5

AESアルゴリズムとは(リトライ)

AESルゴリズムはブロック暗号アルゴリズムで、固定長(128bit)のブロックを、128/192/256bitの鍵で暗号化するもの。この暗号化は複数回行われ、各回(ラウンド)ではSubBytes -> ShiftRows -> MixColumns -> AddRoundKeyするもの。それぞれの解説は図が絡むのでしないw 結城先生の「暗号技術入門」を買ってください。実装といっても、そんな難しくはなく、すでにGoでは標準パッケージ「crypto/aes」で容易されているので、それを使ってやればおk。

// AESによる暗号化
func EncryptByBlockSecretKey(key []byte, plainText string) ([]byte, error) {
    c, err := aes.NewCipher(key); if err != nil { // NewCipherで暗号オブジェクトを作る。
        return nil, err
    }
    cipherText := make([]byte, aes.BlockSize)

    c.Encrypt(cipherText, []byte(plainText)) // Input/Output must be 16bits large
    return cipherText, nil
}

func DecryptByBlockSecretKey(key []byte, cipherText []byte) string {
    c, err := aes.NewCipher(key); if err != nil { // NewCipherで暗号オブジェクトを作る。
        fmt.Println(err.Error())
        return ""
    }

    plainText := make([]byte, aes.BlockSize)
    c.Decrypt(plainText, cipherText)

    return string(plainText)
}

func main() {
    plainText := "1234123412341234"

    key := []byte("1234123412341234123412341234123") // あえてAES規格ではない鍵長を使う
    fmt.Println(EncryptByBlockSecretKey(key, plainText) 
        # crypto/aes: invalid key size 31                                       <- 暗号オブジェクト作成時にKeySizeErrorが発生

        key = []byte("1234123412341234") // AES-128
    fmt.Println(EncryptByBlockSecretKey(key, "12341234123412345")) // Longer than 16 byte
        # [196 235 186 96 98 151 252 89 132 220 117 226 229 247 4 48] <nil> <- 1ブロック以上は処理対象にならない

    fmt.Println(EncryptByBlockSecretKey(key, "123412341234123")) // Shorter than 16 byte
        # panic: crypto/aes: input not full block

    cipherText,_ := EncryptByBlockSecretKey(key, plainText)
    fmt.Println(cipherText)                     
        # [196 235 186 96 98 151 252 89 132 220 117 226 229 247 4 48]

        plainText = DecryptByBlockSecretKey(key, cipherText)
    fmt.Println(plainText)
        # 1234123412341234 <- 復号できた。
}

しかし、これでは固定長の平文しか処理できない。固定長以上の平文を暗号化したい場合、16byteのブロック長にきって暗号化を繰り返せばいいのだが、これをブロック暗号のモードを実装しなければいけない。これを次回は実装してみる。

追記( @bata_24さん ありがとうございます)_

  • 復号化 vs 復号 前職のパイセンにもよく言われていたが、本来"復号化”は国語的におかしいみたいだ。なぜなら「復号」は暗号文を元に戻すという意味の動詞だからそうだ。暗号は名詞以外の定義はない。よって、暗号化の対義語は復号になる。ただし、結城本は復号化だが...って思ってたら、字面の対称性を重視しているらしい*6。ま、職場でもよく復号化...と言われてるが、とりあえず本投稿は復号に寄せてる。怒られそうだけど、正直どっちでもいい

  • Rijndael vs AES Rijndaelは厳密には128, 160, 192, 224, and 256bitsの鍵長をサポートしている。また、128, 160, 192, 224, and 256bitsのブロックサイズもサポートしている。AESは鍵長を128,192 and 256bits、ブロックサイズを128bitsに絞った点で異なる。

暗号解読〈上〉 (新潮文庫)

暗号解読〈上〉 (新潮文庫)

暗号解読 下巻 (新潮文庫 シ 37-3)

暗号解読 下巻 (新潮文庫 シ 37-3)

暗号技術入門 第3版 秘密の国のアリス

暗号技術入門 第3版 秘密の国のアリス

Github + CircleCI + DockerでGCEを動かす - ファイル暗号化 on Github編

  • 先に断っておくと本編はあまり、DockerとGCE関係ない。
  • 見られたくないファイルをGithubにあげたい場合、Github上では暗号化してCircleCIのビルドのタイミングで複合化したいニーズがあると思う。
  • 例えば 証明書をNGINXイメージに埋め込むとか。
  • なのでGithubにprするときにはAESアルゴリズムで暗号化すればどうカナ〜と思った。
  • せっかくなのでGoで
  • 単純で。こうやればおk。パッケージがあるのが素晴らしい。っていうか自力ではじめて暗号化するコード書いたな。
  • CBCモード使わないと、出力も入力も16byte固定なんすね。普段意識しないから新鮮だった。やはり自分で実装するのはいい。必ず発見があって面白い <- 一番いいたかったこと
func main() {
    // 
    key := []byte("1234123412341234123412341234123") // Not AES-128, 256, 512(bit)
    c, err:= aes.NewCipher(key); if err != nil {
        fmt.Println(err.Error()) // Erro発動
    }

    key = []byte("1234123412341234") // AES-128
    c, err = aes.NewCipher(key); if err != nil {
        fmt.Println(err.Error())
    }

    cipherText := make([]byte, aes.BlockSize)
    plainText := []byte("1234123412341234")

    c.Encrypt(cipherText, plainText) // Input/Output must be 16bits large
    fmt.Println(cipherText)

    c.Decrypt(plainText, cipherText)
    fmt.Println(string(plainText))
}

追記

  • よく考えてらIAMのkey management 使えばいいわ