ヘッダー暗号化付きダブルラチェットをRubyで実装する

ヘッダー暗号化付きダブルラチェット（Double Ratchet with header encryption）をRubyで実装してみる。

このアルゴリズムはSignalプロトコルの主要な構成要素の1つであり、Signalの公式Webサイトに[技術的なホワイトペーパー](https://signal.org/docs/specifications/doubleratchet/)がある。今回はこのドキュメントにできるだけ沿った形での実装を目指す。

動くコードは https://github.com/ts-3156/doubleratchet-ruby に置いています。

## ダブルラチェットアルゴリズムとは

ダブルラチェットアルゴリズム（Double Ratchet Algorithm）は、暗号通信に用いる秘密鍵を随時更新することでその通信における前方秘匿性、後方秘匿性を提供します。

より詳細な説明や暗号化無しのダブルラチェットアルゴリズムについては [ダブルラチェットアルゴリズムをRubyで実装する](https://qiita.com/ts-3156/items/9ef770017ca88345df26) をご参照ください。

## ヘッダー暗号化付きダブルラチェットとは

ダブルラチェットアルゴリズムはメッセージヘッダーを暗号化無しで送信します。メッセージヘッダーにはラチェット公開鍵とメッセージの順序が含まれており、場合によってはこの情報ですら秘匿することが望ましいこともあります。

ヘッダー暗号化付きダブルラチェットでは、送信と受信の両方のメッセージに対してヘッダーキーと次のヘッダーキーを保存することでメッセージヘッダーの暗号化を実現します。このヘッダーキーにはDHラチェットが用いられるためヘッダーキーは随時更新されていきます。

ヘッダー暗号化付きダブルラチェットのより詳細な動作については [技術的なホワイトペーパー](https://signal.org/docs/specifications/doubleratchet/) をご確認ください。分かりやすく解説されています。


:::note alert
暗号化アルゴリズムや暗号化プロトコルにはたくさんの種類があり、誤用しやすく、潜在的な落とし穴がたくさんあります。

安全なセキュリティシステムの設計を正しく行うことは非常に難しく、通常はセキュリティ専門家の領域となります。

設計や実装にほんの小さな間違いがあるだけでセキュリティが完全に無効になる可能性があります。

この記事の作者はセキュリティ専門家ではありません。できるだけ正しい情報を書くよう心掛けていますが、書かれた情報の正しさはご自身で検証してください。
:::

## Rubyで実装したコード

Rubyでの実装にはlibsodiumのRubyバインディングであるRbNaCl gemを用いています。

暗号プリミティブの独自実装はせずにライブラリで用意されているものを利用しています。

実装したRubyコードのうち、特にヘッダー暗号化に関係する部分のみを説明します。

```ruby
def hencrypt(hk, plaintext)
  cipher = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(hk)
  nonce = RbNaCl::Random.random_bytes(cipher.nonce_bytes)
  nonce + cipher.encrypt(nonce, plaintext, '')
end
```

ヘッダーの暗号化にはXChaCha20Poly1305IETFを採用しています。AEAD暗号化方式のAssociated dataは指定されていないので空文字にしています。


```ruby
def hdecrypt(hk, ciphertext)
  return nil unless hk
  cipher = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(hk)
  nonce = ciphertext[0..cipher.nonce_bytes - 1]
  ciphertext = ciphertext[cipher.nonce_bytes..-1]
  cipher.decrypt(nonce, ciphertext, '')
  rescue RbNaCl::CryptoError => e
    if e.message == 'Decryption failed. Ciphertext failed verification.'
      nil
    else
      raise
  end
end
```

メッセージヘッダーが暗号化されているため、公開鍵が変わったことを知るためにメッセージヘッダーを復号化する必要があります。このことから、正常系のフローであっても`hk`が空であったり復号化に失敗することがあります。


```ruby
def ratchet_encrypt_he(state, plaintext, ad)
  state[:cks], mk = kdf_ck(state[:cks])
  header = message_header(state[:dhs_pub], state[:pn], state[:ns])
  enc_header = hencrypt(state[:hks], header.dump)
  state[:ns] += 1
  [enc_header, encrypt(mk, plaintext, concat(ad, enc_header))]
end
```

この関数は暗号化無しの場合とほとんど同じです。

```ruby
def decrypt_header(state, enc_header)
  if (plaintext = hdecrypt(state[:hkr], enc_header))
    return [MessageHeader.parse(plaintext), false]
  end
  if (plaintext = hdecrypt(state[:nhkr], enc_header))
    return [MessageHeader.parse(plaintext), true]
  end

  raise
end
```

戻り値の2つ目の値は公開鍵が変わったかどうかのフラグです。暗号化無しの場合は単に公開鍵を比較するだけでしたが、メッセージヘッダーが暗号化されているとそれができません。
`hkr`（受信ヘッダーキー）と`nhkr`（次の受信ヘッダーキー）のどちらで復号化できたかによって、公開鍵が変わったかどうかを判断しています。

----

残りの関数については暗号化無しの場合と暗号化ありの場合であまり差がないため説明を省略します。

動くコードは https://github.com/ts-3156/doubleratchet-ruby に置いています。

## セキュリティ上の考慮事項

[公式ドキュメント](https://signal.org/docs/specifications/doubleratchet/#security-considerations) をご確認ください。


## 参考にした情報

https://signal.org/docs/specifications/doubleratchet/

https://github.com/RubyCrypto/rbnacl

https://doc.libsodium.org/

https://github.com/ts-3156/doubleratchet-ruby/