pythonで暗号演算 ~ChaCha20-Poly1305で認証付き暗号文生成~

python,暗号演算,暗号演算

今回はAES-GCMに続いて、ChaCha20-Poly1305による認証付き暗号文生成を取り上げます。

概要

ChaCha20-Poly1305は、ChaCha20というストリーム暗号と、Poly1305という認証子生成方式を組み合わせて行う、認証付き暗号文生成方法です。

もともとTLSでメッセージのやり取りをする際、実データを暗号化する方式として使用されていたのはAESのみでした。

そのため、仮にAESの暗号処理に致命的な脆弱性が発覚した場合、現状の通信自体が成り立たなくなってしまいます。

このような状態は単一障害点(SPOF)と言われ、各構成要素の冗長化が必要とされています。

そうしてTLSにおける新しい暗号処理として仕様化されたのが、今回のChaCha20-Poly1305です。

詳細については以下RFC7539を確認してみてください。

ChaCha20

ChaCha20はDanielJ.Bernsteinによって設計されたストリーム暗号です。

256ビット(32バイト)の鍵値32ビット(4バイト)のカウンタ値96ビット(12バイト)のノンスから暗号文を生成します。

通常カウンタは1から開始します。

平文を64バイトのブロックに分割し、ブロックごとにカウンタをインクリメントさせながら暗号化を行います。

Poly1305

128ビット(16バイト)の鍵二つと、可変長のメッセージから128ビット(16バイト)の認証子を生成します。

本鍵値は、カウンター0のChaCha20から算出された値のうち、上位16バイトと下位16バイトを鍵値として使用します。

ノンスの扱い

TLSにおけるChaCha20-Poly1305のノンスの扱いは、AES-GCMとは異なっております。

ChaCha20-Poly1305はTLSの暗号化通信を行う際、サーバクライアント双方で持つシーケンス番号をノンスに利用します。

通信データにノンスを付与する必要がない分、AES-GCMよりデータ長が小さくなります。

AES-GCMについては以下過去記事を参照してください。

実装

以下参考にPyCryptodomeライブラリを使用して、実際にChaCha20-Poly1305による暗号演算を実施します。

ソース

ほぼほぼ前回のAES-GCMと一緒です。

from Crypto.Cipher import ChaCha20_Poly1305
from Crypto.Random import get_random_bytes


key = get_random_bytes(32)
nonce = get_random_bytes(12)
data = b"secret"

def main():
    # ChaCha20-Poly1305暗号化
    ciphertext, mac = ChaCha20_Poly1305_encrypt(key,nonce,data)
    print(ciphertext, mac)
    # ChaCha20-Poly1305復号化
    plaintext = ChaCha20_Poly1305_decrypt(key,nonce,ciphertext,mac)
    print(plaintext)


# ChaCha20-Poly1305暗号化
def ChaCha20_Poly1305_encrypt(key,nonce,text):
    cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce)
    ciphertext, mac = cipher.encrypt_and_digest(text)
    return ciphertext, mac


# ChaCha20-Poly1305復号化
def ChaCha20_Poly1305_decrypt(key,nonce,ciphertext,mac):
    plaintext = 0
    cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce)
    try:
        plaintext = cipher.decrypt_and_verify(ciphertext,mac)
    except (ValueError, KeyError):
        print("Incorrect decryption")
    return plaintext


if __name__ == '__main__':
	main()

性能比較

せっかくなのでAES-GCMChaCha20-Poly1305で処理時間を比較してみました。

from Crypto.Cipher import ChaCha20_Poly1305
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import time

key16 = get_random_bytes(16)
key32 = get_random_bytes(32)
nonce = get_random_bytes(12)
data = b"secret"*1000000

def main():
    # AES_GCM暗号化
    start = time.time()*1000000
    ciphertext, mac = aes_gcm_encrypt(key16,nonce,data)
    print( "AES_GCM暗号化" , int(time.time()*1000000-start), 'μSec' )
    # AES_GCM復号化
    start = time.time()*1000000
    plaintext = aes_gcm_decrypt(key16,nonce,ciphertext,mac)
    print( "AES_GCM復号化" , int(time.time()*1000000-start), 'μSec' )
    # ChaCha20-Poly1305暗号化
    start = time.time()*1000000
    ciphertext, mac = ChaCha20_Poly1305_encrypt(key32,nonce,data)
    print( "ChaCha20-Poly1305暗号化" , int(time.time()*1000000-start), 'μSec' )
    # ChaCha20-Poly1305復号化
    start = time.time()*1000000
    plaintext = ChaCha20_Poly1305_decrypt(key32,nonce,ciphertext,mac)
    print( "ChaCha20-Poly1305復号化" , int(time.time()*1000000-start), 'μSec' )


# AES_GCM暗号化
def aes_gcm_encrypt(key,iv,text):
    cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
    ciphertext, mac = cipher.encrypt_and_digest(text)
    return ciphertext, mac


# AES_GCM復号化
def aes_gcm_decrypt(key,iv,ciphertext,mac):
    plaintext = 0
    cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
    try:
        plaintext = cipher.decrypt_and_verify(ciphertext,mac)
    except (ValueError, KeyError):
        print("Incorrect decryption")
    return plaintext

# ChaCha20-Poly1305暗号化
def ChaCha20_Poly1305_encrypt(key,nonce,text):
    cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce)
    ciphertext, mac = cipher.encrypt_and_digest(text)
    return ciphertext, mac


# ChaCha20-Poly1305復号化
def ChaCha20_Poly1305_decrypt(key,nonce,ciphertext,mac):
    plaintext = 0
    cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce)
    try:
        plaintext = cipher.decrypt_and_verify(ciphertext,mac)
    except (ValueError, KeyError):
        print("Incorrect decryption")
    return plaintext


if __name__ == '__main__':
	main()

結果は以下となり、ChaCha20-Poly1305よりもAES_GCMの方がパフォーマンス面で優れているように見えます。

AES_GCM暗号化 6981 μSec
AES_GCM復号化 5984 μSec
ChaCha20-Poly1305暗号化 29891 μSec
ChaCha20-Poly1305復号化 30495 μSec

しかし、一概にAES_GCMの方が性能が優れているとは言えず、処理速度はハードウェア性能に依存するらしいです。