ちいさな金庫のファイル形式(公開仕様)
KAGI v1 / KAGIBAK v1 ・ 最終更新日: 2026年9月25日
ちいさな金庫は、写真・動画を次の形式で保存します。アプリがなくなっても、合言葉があれば中身を取り出せるように公開しています。
数値はすべてリトルエンディアンです。暗号は AES-256-GCM(ノンス 12 バイト、タグ 16 バイト)、HKDF-SHA256、HMAC-SHA256、PBKDF2-HMAC-SHA256 だけを使います。
1. 鍵
| 名前 | 作り方 |
|---|---|
| VK(金庫の鍵) | 256 bit の乱数。端末のキーチェーン(この端末だけ)に保存 |
| ファイル鍵 | HKDF-SHA256(IKM = VK, salt = itemID の16バイト, info = "kagi/v1/file/<kind>", 32バイト)。kind は 1(原本)または 2(サムネイル) |
| カタログ鍵 | HKDF(VK, salt = vaultID の16バイト, info = "kagi/v1/catalog") |
| KCV 用の鍵 | HKDF(VK, salt = なし, info = "kagi/v1/kcv") |
| 重複検知の鍵 | HKDF(VK, salt = なし, info = "kagi/v1/dedupe") |
| バックアップの MAC 鍵 | HKDF(VK, salt = なし, info = "kagi/v1/backup-mac") |
| KEK(合言葉の鍵) | PBKDF2-HMAC-SHA256(UTF-8(NFKC(合言葉) の前後の空白を削ったもの), salt 16バイト, 反復回数(既定 600,000), 32バイト) |
- 包んだ VK:
AES-GCM(KEK).seal(VK, AAD = "kagi/v1/wrap|" + vaultID の大文字 UUID 文字列)の combined(ノンス ‖ 暗号文 ‖ タグ)。 - KCV:
AES-GCM(KCV 用の鍵).seal("kagi-kcv-v1")の combined。 - アルバム名:
AES-GCM(カタログ鍵).seal(UTF-8 の名前, AAD = "kagi/v1/album|" + アルバム ID の大文字 UUID 文字列)。
2. vault.json(金庫フォルダ。平文。秘密は含まない)
{"format":1,"vaultID":"…","createdAt":"2026-09-25T09:00:00Z","kcv":"<base64>",
"wrap":{"alg":"pbkdf2-sha256","iterations":600000,"salt":"<base64>","wrappedKey":"<base64>"}}
控えを index/vault.json.bak に置きます。
3. 暗号化ファイル(KAGI v1)
置き場所: objects/<先頭2文字>/<itemID>.kga、thumbs/…/<itemID>.kgt
ヘッダ 64 バイト
0 "KAGI" 4B
4 formatVersion u8 1
5 kind u8 1=原本, 2=サムネイル(長辺400px の JPEG)
6 flags u16 0
8 itemID 16B UUID のバイト列
24 chunkSize u32 1,048,576
28 plaintextLength u64
36 chunkCount u32 ceil(plaintextLength / chunkSize)。0バイトなら 1
40 reserved 24B 0
メタ情報ブロック
u32 length ‖ AES-GCM combined(ファイル鍵、AAD = ヘッダ64B ‖ "meta")
平文は JSON: {"v":1,"itemID","mediaType":"photo|video","uti","originalFilename","captureDate","importDate",
"pixelWidth","pixelHeight","duration","isLivePhotoStill"}(日時は 1970 年からの秒数)
チャンク i = 0 … chunkCount-1
ノンス 12B ‖ 暗号文(最後以外は chunkSize)‖ タグ 16B
AAD = ヘッダ64B ‖ u32 i ‖ u8 isFinal(最後のチャンクだけ 1)
ファイルの末尾に余分なバイトがあってはいけません。
4. バックアップ(KAGIBAK v1、.kagibackup)
"KAGIBAK1"(8B) ‖ u32 headerLength(64KB 以下)‖ ヘッダ JSON
{"format":1,"vaultID":…,"createdAt":…,"kdf":{"alg":"pbkdf2-sha256","iterations":…,"salt":b64},"wrappedKey":b64}
エントリ(くり返し)
u8 type(1=原本, 2=サムネイル, 3=カタログ)‖ 16B itemID(カタログはゼロ)‖ u64 length ‖ 中身
原本・サムネイル: .kga / .kgt のバイト列をそのまま
カタログ: AES-GCM(カタログ鍵(バックアップ元の vaultID), AAD = "kagi/v1/backup-catalog") で暗号化した JSON
{"v":1,"albums":[{"id","name","createdAt","sortIndex","coverItemID","kind"}],"items":[{"id","albumID","trashedAt"}]}
終端
u8 0xFF ‖ HMAC-SHA256(バックアップの MAC 鍵、先頭から 0xFF の直前まで)
アプリはカタログを最初のエントリとして書き、復元ではカタログが原本より前にある前提でアルバムを割り当てます(後ろにある場合、その件は「はじめのアルバム」に入ります)。
取り出しの手順(概要)
.kagibackupのヘッダ JSON から salt・反復回数・wrappedKey を読み、合言葉から KEK を作って VK を取り出す(AAD は"kagi/v1/wrap|" + vaultID)。- VK からバックアップの MAC 鍵を作り、末尾の HMAC を確かめる。
- 各エントリの
.kgaについて、itemID からファイル鍵を作り、メタ情報ブロックとチャンクを順に復号してつなげる。