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

Got Some \W+ech?

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

2016年の振り返りと2017年の目標

振返り

Keep

  • ジムにコンスタントにいくこと
  • 登壇すること
  • 勉強会にもよく参加してた
  • Docker/SIEM/AWS/GCPなどに取り組んだのは良かった。
  • Goを始めたのもよかった。
  • 本は結構読んだ

Problem

  • やる気にムラがあった
  • お金系(401k, NISA使い切らない)などがなかった
  • ニュースソース・新聞を積極的に読んでなかった。忙しいとめんどくさがる
  • ブログ/Qiita投稿などのアウトプットが2〜11月までなかった。
  • 実家に中々帰れない。
  • 将棋を8月頃に一回手放す。
  • 個人Android/Webアプリを作れてません
  • 機械学習も結局てを動かせず

Try

  • 自分のスケジュールをもっとタイトに切る(コントロールする)。家が近いので深夜遅くまで働いてしまい、結果パフォーマンスとペースを崩してしまった。具体的には24時までに就寝し、6時起きを心がける。翌日のスケジュールは24時までに。
  • 実家にコンスタントに帰るのは諦め、記念日を大切にするスタイルにする
  • 持久力をつける(走ることを始める)
  • 1〜2ヶ月に1つのテーマで技術勉強にとりくみ、それを少しづつアウトプットするといいかもしれない
  • CISSPをやるか、機械学習をがんばるか、自分のアプリを作るか。毎日やれることと、週末などそれなりにまとまった時間が必要そうなもので区切れそう。
  • 新聞を定期的に読む
  • CISSP <- とる。
  • 機械学習 <- 作る。自分の業務を楽にするものを作る。
  • chatbot <- 作る。自分の業務を楽にするものを作る。
  • サービス <- 作る(アイディアはあるけど手を動かせて無い状態)。クローラー
  • AR <- やりたい

Googleが買収したセキュリティ企業

2016年12月までにGoogleが買収したセキュリティ企業を調べてみた。きれいな案件だけでなく、潰しにかかってるっぽいものもあって面白かった(小並感。企業買収をExitとするなら、対象企業がカバーできてないソリューションを開発するのがいいという事だけは分かった。アタリマエ。

要約

企業名 種別 買収日 調達額
GreenBorder Technologies エンドポイント防御・仮想化 2007/5/10 $18M
Impermium アンチスパム・不正アクセス対策 2014/1/16
Interactive Security Group レピュテーションススコアリング 2012/6/3 $9M
Postini エンドポイント防御 2007/9/10 $16M
reCAPTCHA 不正アクセス対策 2009/8/6 ??
SlickLogin 不正アクセス対策 2014/2/17 $20K
Spider.io マルウェア検知 2014/2/21 ??
Widevine Technologies 著作権保護 2010/12/3 $44M
Zynamics OSS 2011/3/2 ??
VirusTotal データマイニング 2012/9/9 ??

GreenBorder Technologies

Imperium

  • アンチスパム・オンラインのアカウント防御サービス
  • 不正登録、不正ログイン、アカウントハイジャック、ソーシャルスパムから防御する
  • 買収前はPinterestTumblrが使ってたが、今はサービスを停止している。
    • Google内のアンチスパムグループに統合されたらしい

Interactive Security Group(KikScore)

Postini

  • ウイルス・スパム・フィッシングから、メール・インスタントメッセージ・ウェブ上のメッセージを暗号化することでプライバシーを保護する
  • 又、通信をアーカイブすることでコンプライアンスを保つ
  • postini.comにアクセスするとGsuiteにリダイレクトされるので、Gmailやチャットをアーカイブする機能に統合されたっぽい。

reCAPTCHA

  • 説明不要。これ。
  • 企業名継続案件

SlickLogin

  • 高周波音を使ってウェブサービスのログインをセキュア化するもの。謎。
    • 少なくともGoogleの2段階認証にはまだ登場していない...
  • slicklogin.comにアクセスしようとすると403エラーになる。謎

Spider.io

  • 不正なオンライン広告を経由して感染するマルウェアに対するソリューション
    • 感染したPCを検知することで、オンライン広告が世にばまかれることを防ぐ

Widevine

VirusTotal

  • え、Googleが買収してたの?
  • 説明いる?要らない気がする。これ。

  • 企業名継続案件その3

Zynamics

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)バイト文のパディングを追加する必要がある。次回は、それをかく。