「AESで暗号化した時の文字列の長さってどれくらい変わるの?」という質問に答えようと調べた話

経緯

「AESで暗号化した時の文字列の長さってどれくらい変わるんだろう、って調べているけど全然出てこねえ。実際試してみると1.5倍くらいっぽいが。」

という質問が友人から投げられてきたので調べました。

前提

  • AES256
  • 平文は 99 文字

調査ログ

padding のせいだよ説

AESはブロック暗号方式であるので、ブロックごとの長さに足りない分をpaddingされるだけではないか?と予想。 AESのブロック長は 128bit = 16byte なので Math.ceil(99/16) * 16 = 112 で 112 文字になるのでは?

→ 1.5倍だから150文字くらいになっているのだろう……おかしい

99文字とはいったが1文字1byteとは限らないのでは?

→ ascii 範囲内であることが判明。utf-16とかじゃなければ1文字1byteになりそう(´・ω・`) 99文字 → 152文字らしいので差が53byteある。そもそもブロック長以上の差分が出るわけがないのでおかしい。1

padding 説は濃厚だがそれだけではなさそう。

padding って実際なにしてんの

ここでインターネットに本格的に頼りだす。記事とそこで挙げられている参考実装を読む

www.atmarkit.co.jp

github.com

参考実装だと 128 bit ごとに入れてるのでそれより長い長さの暗号文をどう扱うかが問題。
もう実装読むしかねえという気持ちになり javax.crypto のソースを漁ると Cipher.getInstance("AES/CBC/PKCS5Padding"); という記述を発見。
また、本人から参考にしている Qiita 記事をもらう。

qiita.com

PKCS5Padding

どうも名のある padding 方式らしいので調べたらRFCとかにもある模様。PKCS#5, PKCS#7のバリエーションがあって、基本的に余った文字数が1だったら 0x01で、2だったら 0x02でっていうふうに埋めてく模様。

blog.shin1x1.com

余談ですが、Java の javax.crypto.Cipher にある PKCS5Padding は、名前は PKCS5 ですが、実質は PKCS#7 相当の動きをするようです。

ひどい話だ……。実用上あんまり問題はないんだろうが。やはりこれを読んでても padding でやたら長くなるのはおかしそうだ。

base64 で膨れてる説

Q: もしかして暗号文って最後に==って出てません?

A: 出てるよ

base64 エンコードされていたことが判明。base64エンコードは長さが変わるのでこれっぽい。 平文→暗号化に入るとき(128bit単位にパディングされて暗号化)→暗号化(パディングされたのと長さ同じ)→base64

というわけで 99 bytes を変換したら 152 bytes になるかを検証。

平文: 99 bytes
平文(パディング): 112 bytes
暗号文: 112 bytes
base64: 152 bytes ( = Math.ceil(Math.ceil(112 * 8 /6) / 4) * 4 )

ok

そんでもとの質問は「AESで暗号化した時の文字列の長さってどれくらい変わるの?」だったので、ざっくり1.5倍かなーと思いつつ試算

  4(n+16)/3 >= 3n/2
⇔ 128 >= n

ダメじゃん!2

仮に2倍とっておくと 8>=n におさえられて安心。
input の平文の長さが決まってれば 2 倍じゃなくてかなりきれいに扱えそう。

実験

ここまで理解したところで実験。

encrypted len - Google スプレッドシート

たしかに1.5倍だとところどころ抑えられていないことを確認。 とゆーわけで input の長さによるが、2倍あればおおむねいける。固定長ならもっと攻めることができる、という感じでした。

# https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aCipher.html
require 'openssl'
require 'base64'

class Encryptor
  def initialize(data)
    @salt = OpenSSL::Random.random_bytes(8)
    @pass = "**secret password**"
    @data = data
  end

  def exec
    # 暗号化器を作成する
    enc = OpenSSL::Cipher.new("AES-256-CBC")
    enc.encrypt
    # 鍵とIV(Initialize Vector)を PKCS#5 に従ってパスワードと salt から生成する
    key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(@pass, @salt, 2000, enc.key_len + enc.iv_len)
    key = key_iv[0, enc.key_len]
    iv = key_iv[enc.key_len, enc.iv_len]
    # 鍵とIVを設定する
    enc.key = key
    enc.iv = iv

    # 暗号化する
    encrypted_data = ""
    encrypted_data << enc.update(@data)
    encrypted_data << enc.final

    encrypted_data
  end
end

1.upto(255) do |n|
  enc = Encryptor.new('a' * n).exec
  enc_size = enc.size
  base64_size = Base64.encode64(enc).size

  puts "#{n},#{enc_size},#{base64_size}"
end

  1. ここで utf-8 は ascii 範囲内の文字は ascii 互換で 1byte 文字であること、日本語の「あ」とかは3byte文字であることなどを説明

  2. というか 99 -> 152 の時点で 1.5倍超えてる