browser-file-cryptoライブラリを公開しました.
TimeFileのセキュア共有機能のために開発したクライアントサイドファイル暗号化モジュールをオープンソースとして公開します。

TimeFileのセキュア共有機能で使っていた暗号化コードをオープンソースとして公開します。
ブラウザでファイルを暗号化するのは、思ったより面倒です。 Web Crypto APIを直接使うと、saltを生成して、IVを作って、鍵を導出して...毎回同じような100行以上のコードを書くことになります。 TimeFileでは、この作業を減らすために内部でモジュールを開発していました。より多くの方のお役に立てればと思い、私たちが使っていたコードを整理してオープンソースとして公開することにしました。
インストール
1npm install @time-file/browser-file-crypto
1pnpm add @time-file/browser-file-crypto
1yarn add @time-file/browser-file-crypto
使い方
1import { encryptFile, decryptFile } from '@time-file/browser-file-crypto';23// 暗号化4const encrypted = await encryptFile(file, { password: 'secret' });56// 復号化7const decrypted = await decryptFile(encrypted, { password: 'secret' });
なぜ作ったのか
TimeFileにはセキュア共有機能があります。 ファイルをアップロードする時にパスワードを設定すると、サーバーには暗号化されたファイルだけが保存され、パスワードを知っている人だけがダウンロードできます。 これを実現するには、クライアント側でファイルを暗号化する必要があります。サーバーが平文を見てはいけないからです。 最初はcrypto-jsを使おうと思ったのですが、開発が止まっていました。しかも内部的にMath.random()を使っている部分があり、セキュリティ上推奨されていませんでした。 aws-cryptoも見てみましたが、Safariで互換性の問題があり、バンドルサイズが200KBを超えていました。ファイル暗号化のためだけにそこまで入れるのはちょっと...という感じでした。 結局、Web Crypto APIを直接使って実装することにしました。
Web Crypto APIを直接使うとこうなります
参考までに、Web Crypto APIを直接使うとこんなコードを書くことになります:
1const salt = crypto.getRandomValues(new Uint8Array(16));2const iv = crypto.getRandomValues(new Uint8Array(12));34const keyMaterial = await crypto.subtle.importKey(5 "raw",6 new TextEncoder().encode(password),7 "PBKDF2",8 false,9 ["deriveKey"],10);1112const key = await crypto.subtle.deriveKey(13 {14 name: "PBKDF2",15 salt: salt.buffer,16 iterations: 100000,17 hash: "SHA-256",18 },19 keyMaterial,20 { name: "AES-GCM", length: 256 },21 false,22 ["encrypt"],23);2425const ciphertext = await crypto.subtle.encrypt(26 { name: "AES-GCM", iv },27 key,28 data,29);3031// ここで終わりではなく、saltとIVを結果に付けないと後で復号化できない32const result = new Uint8Array(1 + 16 + 12 + ciphertext.byteLength);33result[0] = 0x01; // marker34result.set(salt, 1);35result.set(iv, 17);36result.set(new Uint8Array(ciphertext), 29);
暗号化だけでこれだけ書く必要があります。 復号化は別途書かないといけないですし。 browser-file-cryptoを使えば、これが2行になります!
機能
パスワード暗号化
一番基本的な使い方です。パスワードを渡すとPBKDF2で鍵を導出し、AES-256-GCMで暗号化します。
1const encrypted = await encryptFile(file, { password: 'my-password' });2const decrypted = await decryptFile(encrypted, { password: 'my-password' });
キーファイル暗号化
パスワードの代わりに256ビットのランダムキーを使うこともできます。 パスワードよりセキュリティが高いです。
1import { generateKeyFile, encryptFile } from '@time-file/browser-file-crypto';23const keyFile = generateKeyFile();4// keyFile.keyにbase64エンコードされた256ビット鍵が入っています56const encrypted = await encryptFile(file, { keyData: keyFile.key });
TimeFileでは、このキーを.tfkeyファイルとしてダウンロードできるようにして、後で復号化する時にアップロードしてもらう形にしています。
進捗コールバック
大きなファイルを暗号化する時にプログレスバーを表示したい場合は、onProgressを使います。
1await encryptFile(file, {2 password: "secret",3 onProgress: ({ phase, progress }) => {4 // phase: 'deriving_key' | 'encrypting' | 'decrypting' | 'complete'5 updateProgressBar(progress);6 },7});
暗号化タイプの検出
暗号化されたファイルがパスワードで暗号化されたのか、キーファイルで暗号化されたのかを確認できます。
1import { getEncryptionType } from "@time-file/browser-file-crypto";23const type = getEncryptionType(encryptedBlob);4// 'password' | 'keyfile' | 'unknown'
TimeFileではこれを使って復号化のUIを切り替えています。パスワード暗号化ならパスワード入力欄を、キーファイル暗号化ならキーファイルアップロードボタンを表示する、という感じです。
エラーハンドリング
パスワードが間違っていたり、ファイルが壊れている場合はCryptoErrorが発生します。
1import { CryptoError } from "@time-file/browser-file-crypto";23try {4 await decryptFile(encrypted, { password: "wrong" });5} catch (error) {6 if (error instanceof CryptoError) {7 if (error.code === "INVALID_PASSWORD") {8 alert("パスワードが違います");9 }10 }11}
エラーコードにはINVALID_PASSWORD、INVALID_KEYFILE、INVALID_ENCRYPTED_DATAなどがあります。
技術的な詳細
暗号化スペック
- アルゴリズム: AES-256-GCM
- 鍵導出: PBKDF2 (100,000イテレーション, SHA-256)
- Salt: 16バイト (毎回ランダム生成)
- IV: 12バイト (毎回ランダム生成)
AES-GCMは認証付き暗号化(Authenticated Encryption)なので、データの改ざんも検出できます。誰かが暗号化されたファイルを改ざんすると、復号化時に失敗します。
ファイルフォーマット
暗号化されたファイルの構造はこうなっています:

バンドルサイズ
gzip後で約5KBです。外部依存がなくWeb Crypto APIだけを使っているので軽量です。 crypto-jsが50KB、aws-cryptoが200KB以上であることを考えると、かなり小さい方だと思います。
ブラウザサポート
Web Crypto APIをサポートするすべてのブラウザで動作します。Chrome、Firefox、Safari、Edge、すべて対応しています。 Node.js 18以上でも使えます。webcryptoが組み込まれているからです。
今後の予定
現在はファイル全体をメモリに読み込んで暗号化する方式です。小さいファイルは問題ないですが、数GBのファイルだとブラウザのメモリが足りなくなる可能性があります。 次のバージョンではストリーミング暗号化をサポートする予定です。 Web Workerサポートも検討しています。今はメインスレッドで暗号化しているので、大きなファイルを処理する時にUIが一瞬固まることがあるんです。
リンク
- npm: https://www.npmjs.com/package/@time-file/browser-file-crypto - GitHub: https://github.com/Time-File/browser-file-crypto フィードバックやバグレポートはGitHub Issuesにお願いします。


