logo

202523

ブラウザだけで完結する暗号化ファイルマネージャを作ってみた

ちょっとした思いつきで、ブラウザだけでファイルを暗号化して保管できるアプリを作れるのではないかと思い作ってみました。使い道はよくわからないですが、何かの役に立つこともあるかもしれません。

アプリ: https://sogo.dev/local-file-locker/
ソースコード: https://github.com/SogoKato/local-file-locker

ファイルを選んで encrypt ボタンを押すと……

local_file_locker_1

ファイルが暗号化されて保存されます!

local_file_locker_2

ファイル名をクリックすると復号して表示できます。

local_file_locker_3

以下のブラウザで動作確認しています。

  • Firefox 134.0.2 (macOS, Android)
  • Chrome 132.0.6834 (macOS, Android)

ポイント

WASM でファイルを AES 暗号化

JS にも SubtleCrypto API が用意されていますが、今回は Rust の暗号化ライブラリである aes_gcm を使用してファイルを暗号化する関数を作り、WebAssembly 化して JS から呼び出すようにしてみました。WASM を使ったのは使ってみたかった理由が大きいですが、その方が高速かもという期待もあります。これから SubtleCrypto とのパフォーマンス比較とかもやってみたいなと思います。

処理自体は一般的な AES-GCM 暗号化です。ライブラリを呼んでいるだけなのでコードはこれだけです。

use aes_gcm::{AeadCore, Aes256Gcm, Key};
use aes_gcm::aead::{Aead, KeyInit, OsRng};
use sha2::digest::generic_array::GenericArray;
use sha2::{Sha256, Digest};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn encrypt(password: &str, file: &[u8]) -> Box<[u8]> {
    let key = hash_password(password);
    let cipher = Aes256Gcm::new(&key.into());
    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);

    let ciphertext = cipher.encrypt(&nonce, file).expect("encryption failure!");
    [nonce.as_slice(), &ciphertext].concat().into_boxed_slice()
}

#[wasm_bindgen]
pub fn decrypt(password: &str, file: &[u8]) -> Box<[u8]> {
    let key = hash_password(password);
    let cipher = Aes256Gcm::new(&key.into());

    let (nonce_bytes, ciphertext) = file.split_at(12);
    let nonce = GenericArray::from_slice(nonce_bytes);

    let plaintext = cipher.decrypt(&nonce, ciphertext).expect("decryption failure!");
    plaintext.into_boxed_slice()
}

fn hash_password(password: &str) -> Key<Aes256Gcm> {
    let hash = Sha256::digest(password.as_bytes());
    Key::<Aes256Gcm>::from_slice(&hash).clone()
}

wasm_bindgenUint8Array ↔️ &[u8] Box[u8] のような複雑な型の変換までしてくれるので感動しました。

OPFS でファイルを保管

OPFS (Origin Private File System) は、モダンなブラウザで利用できる origin ごとに隔離された、パフォーマンスとセキュリティに優れたファイルシステムです。

TypeScript から利用するときに tsconfig.json の compilerOptions.lib に dom.asynciterable を追加しないと FileSystemDirectoryHandleentries() メソッド等が使えないという罠があって少し引っかかりました。

今後やりたいこと

  • SubtleCrypto とのパフォーマンス比較
  • Web Worker 化?
    • 2025年2月現在、Safari では FileSystemSyncAccessHandle しかサポートされていないが、これは Web Worker 内でしか利用できない