Got Some \W+ech?

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

大陽日酸に対するサイバー攻撃についてまとめてみた

イントロ

  • 工場や医療現場で使われる酸素や窒素、二酸化炭素などを製造、販売する国内最大手の大陽日酸サイバー攻撃に合っていた模様。報道された2017/1/1には対応済みっぽい。

タイムライン

日時 出来事 備考
2015/11 最遅でもこの時点で侵入済み
2016/3/11 外部からの不正接続(海外?)を確認。ウイルス感染を確認。サイバー攻撃と認知
2016/3下旬 ウイルスを駆除
2016/4/28 警視庁に被害相談
2016/4/29 流出範囲(可能性)の特定
2017/1/1 公開報道。ホームページには未記載
2017/1/5 ホームページに公式発表公開。

攻撃の概要

ぱっと見、標的型攻撃っぽいところはある

攻撃の影響と成否

以下の事実が確認されている。

  • 管理者権限の奪取

  • システム内の情報が外部から見れる状態。600台に対して、外部から管理者権限によるログインが可能だった。

  • 情報の流出は未確認、1万人の社員情報が流出した可能性あり

武器化

Delivery

Exploit

Install

成功 -> 最終的に4種類のマルウェアに感染していたところから。

C&C

(2015年11月までに)成功 -> 外部からの管理者権限を使った遠隔操作で、システム内の大半にあたる6百数十台のサーバーに接続できる状態。サーバーの一つから2回にわたり外部と不正通信が発生。

データの破壊・盗難

成功(可能性が高い) -> グループ国内従業員ら11,105人分の個人情報(社名・氏名・職位・メアド)など内部情報を複数の圧縮ファイルにまとめていた痕跡。サーバーの一つから2回にわたり外部と不正通信が発生。

被害額

窃取された情報から、直接的な被害額は発生していない。 ちなみに株価(1/4)時点では下がるどころか上がってる。世間が気づいてないか、気づいてるけど大して重要視してないか、それともトランプ効果があるからか、これから下がるのか。 f:id:kengoscal:20170104110344p:plain

原因

対策

  • サイバー攻撃早期検知のための監視体制強化
  • 管理者権限の搾取対策今日か
  • 不正遠隔操作を某氏するための対策強化
  • 大陽日酸ではないが、窃取された社員情報を利用した標的型攻撃に気をつける必要はありそう。
  • 例えば「Title: [大陽日酸からの重要なお知らせ] Body: Hoge部長のFugaです。この度は云々カンヌン」みたいな。

所感

  • piyologはやすぎぃ! やっぱりpiyologはすごい。必ず1歩先をいかれている。
  • 後、具体的な数字(600台とか1万人の社員とかサーバ内情報とか)が出てて凄い。どこにあったんだろう 公開情報。
  • 2017/1/1に報道公開した意図は不明。
  • 株価への影響をみたい。1/4に要チェキ
  • これ以上、続報でなさそう。
  • 最初に報道したところが、次の報道も早いと考えたほうがいいかも?
  • JTBの時もそうだが、nikkeiは遅いかも。
  • "当社の生産設備制御系システムは、基本的には今回サイバー攻撃を受けた情報系ネットワークおよびインターネットとは接続されておらず、今後もサイバー攻撃の影響を受けないと考えます。" おっ、そうだな。
  • 警視庁公安部が不正アクセス容疑で操作...あっ(察し)

反省

更新内容

  • 2017/1/1: 初版
  • 2017/1/3: 多数のニュースサイトで取り上げられるも、新しい情報なし。ホームページへの記載もまだ。
  • 2017/1/4: 太陽日酸 -> 大陽日酸。ワロッシュ。どうりで検索も引っかからないはずやで。
  • 2017/1/5: ホームページ上で公式発表。

Ref

ガス大手にサイバー攻撃 警視庁が捜査 - 産経ニュース

ガス大手にサイバー攻撃 ウイルス感染、警視庁捜査 | どうしんウェブ/電子版(社会)

ガス大手にサイバー攻撃、管理者権限奪われる : 社会 : 読売新聞(YOMIURI ONLINE)

ガス大手にサイバー攻撃 | ロイター

http://www.tn-sanso.co.jp/jp/_documents/info_07830547.pdf

通信記録の保存不十分…サイバー攻撃調査できず : 社会 : 読売新聞(YOMIURI ONLINE)

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は又今後書くつもり。