use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
const IV_LEN: usize = 12;
const STORAGE_KEY: &str = "lh_enc_key_v1";
pub(crate) async fn seal(plaintext: &[u8]) -> Option<Vec<u8>> {
let key = device_key().await.ok()?;
encrypt(&key, plaintext).await.ok()
}
pub(crate) async fn open(data: &[u8]) -> Option<Vec<u8>> {
let key = device_key().await.ok()?;
decrypt(&key, data).await.ok()
}
pub(crate) async fn seal_with_raw_key(raw: &[u8; 32], plaintext: &[u8]) -> Option<Vec<u8>> {
let key = import_aes_key(raw).await.ok()?;
encrypt(&key, plaintext).await.ok()
}
pub(crate) async fn open_with_raw_key(raw: &[u8; 32], data: &[u8]) -> Option<Vec<u8>> {
let key = import_aes_key(raw).await.ok()?;
decrypt(&key, data).await.ok()
}
async fn device_key() -> Result<web_sys::CryptoKey, String> {
let raw = load_or_create_key_bytes()?;
import_aes_key(&raw).await
}
fn load_or_create_key_bytes() -> Result<[u8; 32], String> {
let window = web_sys::window().ok_or("no window")?;
let storage = window
.local_storage()
.map_err(|_| "no localStorage")?
.ok_or("no localStorage")?;
if let Ok(Some(hex)) = storage.get_item(STORAGE_KEY) {
if let Some(bytes) = hex_to_32(&hex) {
return Ok(bytes);
}
}
let crypto = window.crypto().map_err(|_| "no crypto")?;
let bytes = [0u8; 32];
let view = unsafe { js_sys::Uint8Array::view(&bytes) };
crypto
.get_random_values_with_array_buffer_view(&view)
.map_err(|_| "getRandomValues failed")?;
drop(view);
let _ = storage.set_item(STORAGE_KEY, &hex32(&bytes));
Ok(bytes)
}
async fn import_aes_key(raw: &[u8]) -> Result<web_sys::CryptoKey, String> {
let window = web_sys::window().ok_or("no window")?;
let crypto = window.crypto().map_err(|_| "no crypto")?;
let subtle = crypto.subtle();
let key_data = js_sys::Uint8Array::from(raw);
let algo = js_sys::Object::new();
let _ = js_sys::Reflect::set(&algo, &JsValue::from_str("name"), &JsValue::from_str("AES-GCM"));
let usages = js_sys::Array::new();
usages.push(&JsValue::from_str("encrypt"));
usages.push(&JsValue::from_str("decrypt"));
let promise = subtle
.import_key_with_object("raw", &key_data.buffer(), &algo, false, &usages)
.map_err(|e| format!("importKey: {e:?}"))?;
let result = JsFuture::from(promise)
.await
.map_err(|e| format!("importKey await: {e:?}"))?;
result
.dyn_into::<web_sys::CryptoKey>()
.map_err(|_| "importKey did not return CryptoKey".into())
}
async fn encrypt(key: &web_sys::CryptoKey, plaintext: &[u8]) -> Result<Vec<u8>, String> {
let window = web_sys::window().ok_or("no window")?;
let crypto = window.crypto().map_err(|_| "no crypto")?;
let subtle = crypto.subtle();
let iv_bytes = [0u8; IV_LEN];
let iv_view = unsafe { js_sys::Uint8Array::view(&iv_bytes) };
crypto
.get_random_values_with_array_buffer_view(&iv_view)
.map_err(|_| "getRandomValues failed")?;
let algo = js_sys::Object::new();
let _ = js_sys::Reflect::set(&algo, &JsValue::from_str("name"), &JsValue::from_str("AES-GCM"));
let _ = js_sys::Reflect::set(&algo, &JsValue::from_str("iv"), &iv_view);
let data = plaintext.to_vec();
let promise = subtle
.encrypt_with_object_and_u8_array(&algo, key, &data)
.map_err(|e| format!("encrypt: {e:?}"))?;
let result = JsFuture::from(promise)
.await
.map_err(|e| format!("encrypt await: {e:?}"))?;
let ciphertext = js_sys::Uint8Array::new(&result);
let ct_bytes = ciphertext.to_vec();
let mut out = Vec::with_capacity(IV_LEN + ct_bytes.len());
out.extend_from_slice(&iv_bytes);
out.extend_from_slice(&ct_bytes);
Ok(out)
}
async fn decrypt(key: &web_sys::CryptoKey, encrypted: &[u8]) -> Result<Vec<u8>, String> {
if encrypted.len() < IV_LEN + 16 {
return Err("ciphertext too short".into());
}
let window = web_sys::window().ok_or("no window")?;
let crypto = window.crypto().map_err(|_| "no crypto")?;
let subtle = crypto.subtle();
let iv = js_sys::Uint8Array::from(&encrypted[..IV_LEN]);
let algo = js_sys::Object::new();
let _ = js_sys::Reflect::set(&algo, &JsValue::from_str("name"), &JsValue::from_str("AES-GCM"));
let _ = js_sys::Reflect::set(&algo, &JsValue::from_str("iv"), &iv);
let ct = encrypted[IV_LEN..].to_vec();
let promise = subtle
.decrypt_with_object_and_u8_array(&algo, key, &ct)
.map_err(|e| format!("decrypt: {e:?}"))?;
let result = JsFuture::from(promise)
.await
.map_err(|e| format!("decrypt await: {e:?}"))?;
let plaintext = js_sys::Uint8Array::new(&result);
Ok(plaintext.to_vec())
}
fn hex32(b: &[u8; 32]) -> String {
let mut s = String::with_capacity(64);
for x in b {
s.push_str(&format!("{x:02x}"));
}
s
}
fn hex_to_32(s: &str) -> Option<[u8; 32]> {
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok()?;
}
Some(out)
}