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) use crate::wallet::{keysync_key_from_entropy, sharedfs_key_from_entropy};
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()
}
pub(crate) async fn ecies_seal(
recipient_pubkey_sec1: &[u8],
plaintext: &[u8],
) -> Option<Vec<u8>> {
let (eph_pub, eph_signer) = crate::wallet::ephemeral_keypair();
let key = crate::wallet::ecdh_shared_key(&eph_signer, recipient_pubkey_sec1).ok()?;
let blob = seal_with_raw_key(&key, plaintext).await?;
let mut out = Vec::with_capacity(eph_pub.len() + blob.len());
out.extend_from_slice(&eph_pub);
out.extend_from_slice(&blob);
Some(out)
}
pub(crate) async fn ecies_open(
device: &k256::ecdsa::SigningKey,
data: &[u8],
) -> Option<Vec<u8>> {
if data.len() < 33 + IV_LEN + 16 {
return None;
}
let (eph_pub, blob) = data.split_at(33);
let key = crate::wallet::ecdh_shared_key(device, eph_pub).ok()?;
open_with_raw_key(&key, blob).await
}
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 mut bytes = [0u8; 32];
crypto
.get_random_values_with_u8_array(&mut bytes)
.map_err(|_| "getRandomValues failed")?;
storage
.set_item(STORAGE_KEY, &hex32(&bytes))
.map_err(|_| "localStorage persist of the device key failed")?;
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 mut iv_bytes = [0u8; IV_LEN];
crypto
.get_random_values_with_u8_array(&mut iv_bytes)
.map_err(|_| "getRandomValues failed")?;
let iv_js = js_sys::Uint8Array::from(&iv_bytes[..]);
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_js);
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 {
crate::encoding::bytes_to_hex(b)
}
fn hex_to_32(s: &str) -> Option<[u8; 32]> {
crate::encoding::hex_to_bytes(s).ok()?.try_into().ok()
}