-
Notifications
You must be signed in to change notification settings - Fork 1
独自に日次キーをダウンロードしてPRI照合するための解析
Tomoaki Masuda edited this page Sep 25, 2020
·
8 revisions
HKDF RFC 5869 ハッシュ計算
Output ← HKDF(Key, Salt, Info, OutputLength)
ハッシュ計算は、不可逆のため元に戻すことができない。
Output ← AES128(Key, Data)
暗号化では、秘密鍵(Key)を共有することで、復号できる。
ENIntervalNumber(Timestamp) <- Tiemstamp/(60*10)
- Timestamp は UTC 基準の Unix秒
- ENIntervalNumber では、10単位のため、60*10 で割った値を使う
- 24時間で TEK が変更になるため、144 固定値
- 将来的に変わる可能性があるが、おそらく変えられない
- デバイスでランダムに生成される 16 bytes のキー
- TEK は14日間保持するため、デバイスには14個のTEKキーが保存されている。
- 正確には、TEKRollingPeriod で増加するが、144 固定のため、1日で切り替わる
- 接触確認が、1日単位のみで検出される制限となる.
tek_i ← CRNG(16)
- 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 を計算することはできない。
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
- 現在使っていないので省略
- 現在使っていないので省略
- 相手のデバイスから渡されたデータが暗号化されている
- 相手の TEK と受信時の RPI がないと復号できない。
ここの部分の詳細を記述する
- 発信者は、1日の最初に TEK_i を生成して保持する。
- 受信者は、BLEで発信者のRPIを受けている。これが濃厚接触時に、RPI_i*として EN API内に保存する
- 発信者は、検査確定時に陽性登録を行い、TEK_i を Key Server に送る
- 陽性登録の翌日、受信者は TEK_i を Key Server からダウンロードする
- 受信者は EN API へ TEK_i を送る。これは、ダウンロードされる TEKs 全てが送られる。
- HKDF 関数を使い、RPIK_i を生成する。
- RPIK_i から、144個分の PaddedDataj を繋げたデータを作る
- AES128 関数を使い、繋げたデータを暗号化する。
- 144分割して RPI_ij に割り当てる
- 受信者が保持している RPI_ij* とすべての RPI_ij を照合させる。
- ここで一致した 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 となっていると思われる。
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;
}
- 暗号化の仕様
- Exposure Notification - Cryptography Specification.pages
- Android EN API の内部コードのスナップショット
- COCOA システム仕様書
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 を返す