Skip to content

独自に日次キーをダウンロードしてPRI照合するための解析

Tomoaki Masuda edited this page Sep 25, 2020 · 8 revisions

用語

HKDF RFC 5869 ハッシュ計算

Output ← HKDF(Key, Salt, Info, OutputLength)

ハッシュ計算は、不可逆のため元に戻すことができない。

AES 暗号化

Output ← AES128(Key, Data)

暗号化では、秘密鍵(Key)を共有することで、復号できる。

ENIntervalNumber, 時刻 ENIN

ENIntervalNumber(Timestamp) <- Tiemstamp/(60*10)

  • Timestamp は UTC 基準の Unix秒
  • ENIntervalNumber では、10単位のため、60*10 で割った値を使う

TEKRollingPeriod

  • 24時間で TEK が変更になるため、144 固定値
    • 将来的に変わる可能性があるが、おそらく変えられない

Temporary Exposure Key, TEK

  • デバイスでランダムに生成される 16 bytes のキー
  • TEK は14日間保持するため、デバイスには14個のTEKキーが保存されている。
    • 正確には、TEKRollingPeriod で増加するが、144 固定のため、1日で切り替わる
    • 接触確認が、1日単位のみで検出される制限となる.
tek_i ← CRNG(16)

Rolling Proximity Identifier Key, RPIK

  • TEK から HKDF を使いハッシュ計算される。
  • 直接表に出てくることはないが、後で RPI を計算するときに使う
  • RPIK_i は、i日目の RPIK という意味。TEK と同じく、1日に1個だけになる
RPIK_i ← HKDF(tek_i, NULL, UTF8("EN-RPIK"), 16)
  • salt が NULL 固定になっている理由は、全デバイスに salt が含まれるため秘匿する理由がないためと思われる
  • ハッシュ計算した後の RPIK_i から、tek_i を計算することはできない。

Rolling Proximity Identifier

RPIK から、10分単位の RPI を生成する。 RPI は、1日単位の RPIK_i と 10分単位の PaddedData_j を組み合わせて AES で暗号化する。

RPI_ij ← AES128(RPIK_i, PaddedData_j)
  • つまり、RPI は1日に144個生成される。
  • この生成した RPI_ij を照合させる

PaddedData_j の構造

  • PaddedDataj[0...5] = UTF8("EN-RPI")
  • PaddedDataj[6...11] = 0x000000000000
  • PaddedDataj[12...15] = ENIN_j

ENIN_j は ENIntervalNumber(Timestamp) となるため、1ずつ加算される。 データはリトルエンディアンで書き込む

ENIN_j = ENIntervalNumber(Timestamp) + j

Associated Encrypted Metadata Key

  • 現在使っていないので省略

Associated Encrypted Metadata

  • 現在使っていないので省略
  • 相手のデバイスから渡されたデータが暗号化されている
    • 相手の TEK と受信時の RPI がないと復号できない。

キーマッチの詳細シーケンス

ここの部分の詳細を記述する

  1. 発信者は、1日の最初に TEK_i を生成して保持する。
  2. 受信者は、BLEで発信者のRPIを受けている。これが濃厚接触時に、RPI_i*として EN API内に保存する
  3. 発信者は、検査確定時に陽性登録を行い、TEK_i を Key Server に送る
  4. 陽性登録の翌日、受信者は TEK_i を Key Server からダウンロードする
  5. 受信者は EN API へ TEK_i を送る。これは、ダウンロードされる TEKs 全てが送られる。
  6. HKDF 関数を使い、RPIK_i を生成する。
  7. RPIK_i から、144個分の PaddedDataj を繋げたデータを作る
  8. AES128 関数を使い、繋げたデータを暗号化する。
  9. 144分割して RPI_ij に割り当てる
  10. 受信者が保持している RPI_ij* とすべての RPI_ij を照合させる。
  11. ここで一致した RPI_ij があるとき、matchKeyCount > 0 として「接触の可能性あり」となる。

RPI_ij の照合は EN API 内で行われているが、このロジックは公開情報のため、自前で実装が可能である。 EN API では、10の時点で TEK_i を返すため、日単位でしか接触時間がわからない。 しかし、自前でRPI照合の処理を行えば、RPI_ijが判明するため j のとき、つまり10分単位での接触時刻が取得可能と思われる。

試しに組んだコード

https://github.com/moonmile/testrpi

コードの抜粋

//////////////////////////////////////////////////////////////
// probeCOCOATek で取得できる TEK DAta
//////////////////////////////////////////////////////////////
let TEKi_S = "80585c0960d903338d22f3ee57250b00"
let TEKi : byte[] = hextobytes(TEKi_S)
//////////////////////////////////////////////////////////////
// UTC の tempstamp は全デバイス共通になる
//////////////////////////////////////////////////////////////
let rolling_start_interval_number = 2665440
// 24時間固定
let rolling_period = 144
// そのまま使う
let ENINi = rolling_start_interval_number 

let EN_PRIK : byte[] = stobytes("EN-RPIK")
let RPIKi = HKDF( TEKi, null, EN_PRIK ,16)  // salt は null 固定

let aes = new AesCryptoServiceProvider();
aes.BlockSize <- 128;
aes.KeySize <- 128;
aes.IV <- [| for i in 0..15 -> byte(0) |]    // IV は null 固定
aes.Key <- RPIKi
aes.Mode <- CipherMode.ECB;
aes.Padding <- PaddingMode.PKCS7;
printfn "key %s IV %s" (bytestohex( aes.Key )) (bytestohex( aes.IV ))
let aes = new AesCryptoServiceProvider();
aes.BlockSize <- 128;
aes.KeySize <- 128;
aes.IV <- [| for i in 0..15 -> byte(0) |]    // IV は null 固定
aes.Key <- RPIKi
aes.Mode <- CipherMode.ECB;
aes.Padding <- PaddingMode.PKCS7;
printfn "key %s IV %s" (bytestohex( aes.Key )) (bytestohex( aes.IV ))

// PaddedDatajを144個分まとめて暗号化する
/// その後で144分割して16バイトにする
/// 最後に16バイト余るが無視でよいらしい
// PaddedDataj[0..5] = "EN-PRI"
// PaddedDataj[6..11] = 0x0
// PaddedDataj[12..15] = ENINj
let PaddedDataj : byte[] = 
    Array.concat [
        for i in 0..143 do
            Array.concat [
                stobytes("EN-RPI") 
                [| for i=6 to 11 do byte(0) |]
                itobytes(ENINi + i )
            ]
    ]
let AES(data: byte[]) =
    let encrypt = aes.CreateEncryptor()
    let encrypted = encrypt.TransformFinalBlock( data, 0, data.Length )
    encrypted
let RPIij = AES(PaddedDataj)
printfn "PaddedDataj len: %d" PaddedDataj.Length
printfn "RPIij len: %d" RPIij.Length
// 144分割する
for i in 0..143 do 
    let RPIj = RPIij.AsSpan(0+16*i,16).ToArray()
    let ENINj = ENINi + i 
    printfn "%d RPI %s" ENINj  (bytestohex( RPIj ))
  • HKDF 関数に渡す salt は常に null でよい
  • AES 関数に渡す IV(初期ベクタ)は null でよい

通常はなんらかの初期値を与えるが、全デバイスで共通となるため、null となっていると思われる。

参照コード

https://github.com/google/exposure-notifications-internals/blob/main/exposurenotification/src/main/cpp/matching_helper.cc#L68

bool MatchingHelper::GenerateIds(const uint8_t *diagnosis_key,
                                 uint32_t rolling_start_number, uint8_t *ids) {
  uint8_t rpi_key[kRpikLength];
  // RPIK <- HKDF(tek, NULL, UTF8("EN-PRIK"), 16).
  if (HKDF(rpi_key, kRpikLength, EVP_sha256(), diagnosis_key, kTekLength,
      /*salt=*/nullptr, /*salt_len=*/0,
           reinterpret_cast<const uint8_t *>(kHkdfInfo), kHkdfInfoLength) != 1) {
    return false;
  }

  if (EVP_EncryptInit_ex(&context, EVP_aes_128_ecb(), /*impl=*/nullptr, rpi_key,
      /*iv=*/nullptr) != 1) {
    return false;
  }

  uint32_t en_interval_number = rolling_start_number;
  for (int index = 0; index < kIdPerKey * kIdLength;
       index += kIdLength, en_interval_number++) {
    *((uint32_t * ) (&aesInputStorage[index + 12])) = en_interval_number;
  }

  int out_length;
  return EVP_EncryptUpdate(&context, ids, &out_length, aesInputStorage,
                           kIdPerKey * kIdLength) == 1;
}

参考資料

シーケンス図のコード

sequenceDiagram

発信者 ->> 発信者: TEK_i
発信者 ->> 受信者: RPI_ij*
受信者 ->> EN API: RPI_ij* を保存
発信者 ->> Key Server: TEK_i
Key Server ->> 受信者: TEK_i
受信者 ->> EN API: TEK_i
EN API  ->>  EN API: RPIK_i <- HKDF(TEK_i)  
EN API  ->>  EN API: 144 個の PaddedDataj を繋げたデータを作る
EN API  ->>  EN API: 暗号化したデータ <- AES128(RPIK_i,j)
EN API  ->>  EN API: 144分割して RPI_ij に保存
loop 照合
    EN API  ->>  EN API: RPI_ij* と RPI_ij がマッチ
end
EN API  ->> 受信者: matchKeyCount > 0 で通知。TEK_i を返す
Loading
Clone this wiki locally