Validation remains blocked by rusqlite u64 conversions in cred/grants.rs, which T19 owns.
63 lines
3.2 KiB
Rust
63 lines
3.2 KiB
Rust
//! WebCrypto interoperability and host key persistence for secret entry.
|
|
use deno_core::{JsRuntime, RuntimeOptions};
|
|
use sausage::cred::entry::{EntryKey, Envelope};
|
|
use std::{os::unix::fs::PermissionsExt, sync::Arc};
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn webcrypto_entry() -> anyhow::Result<()> {
|
|
let dir = tempfile::tempdir()?;
|
|
let path = dir.path().join("entry.key");
|
|
let key = EntryKey::open(&path)?;
|
|
assert_eq!(
|
|
std::fs::metadata(&path)?.permissions().mode() & 0o777,
|
|
0o600
|
|
);
|
|
assert_eq!(key.public_key(), EntryKey::open(&path)?.public_key());
|
|
let mut runtime = JsRuntime::new(RuntimeOptions {
|
|
extensions: vec![
|
|
deno_webidl::deno_webidl::init(),
|
|
deno_web::deno_web::init(
|
|
Arc::new(deno_web::BlobStore::default()),
|
|
None,
|
|
false,
|
|
Default::default(),
|
|
),
|
|
deno_crypto::deno_crypto::init(None),
|
|
],
|
|
..Default::default()
|
|
});
|
|
runtime.execute_script(
|
|
"crypto.js",
|
|
"globalThis.crypto = Deno.core.loadExtScript('ext:deno_crypto/00_crypto.js').crypto;",
|
|
)?;
|
|
runtime.execute_script("encrypt.js", format!(r#"
|
|
globalThis.done = (async () => {{
|
|
const host = await crypto.subtle.importKey('raw', new Uint8Array({}), {{name:'ECDH', namedCurve:'P-256'}}, false, []);
|
|
const pair = await crypto.subtle.generateKey({{name:'ECDH', namedCurve:'P-256'}}, true, ['deriveBits']);
|
|
const bits = await crypto.subtle.deriveBits({{name:'ECDH', public:host}}, pair.privateKey, 256);
|
|
const material = await crypto.subtle.importKey('raw', bits, 'HKDF', false, ['deriveKey']);
|
|
const info = new Uint8Array(Array.from('sausage/secret-entry/v1', c => c.charCodeAt(0)));
|
|
const aes = await crypto.subtle.deriveKey({{name:'HKDF', hash:'SHA-256', salt:new Uint8Array(), info}}, material, {{name:'AES-GCM', length:256}}, false, ['encrypt']);
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
const ciphertext = await crypto.subtle.encrypt({{name:'AES-GCM', iv}}, aes, new Uint8Array(Array.from('browser-only-value', c => c.charCodeAt(0))));
|
|
globalThis.envelope = JSON.stringify({{ephemeral_public_key:Array.from(new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey))), iv:Array.from(iv), ciphertext:Array.from(new Uint8Array(ciphertext))}});
|
|
}})();
|
|
"#, serde_json::to_string(&key.public_key())?))?;
|
|
runtime.run_event_loop(Default::default()).await?;
|
|
let value = runtime.execute_script("result.js", "envelope")?;
|
|
let text = {
|
|
deno_core::scope!(scope, &mut runtime);
|
|
let value = deno_core::v8::Local::new(scope, value);
|
|
value.to_rust_string_lossy(scope)
|
|
};
|
|
let mut envelope: Envelope = serde_json::from_str(&text)?;
|
|
let expected = b"browser-only-value";
|
|
assert!(key.decrypt(&envelope)? == expected);
|
|
let hash = envelope.sha256();
|
|
envelope.ciphertext[0] ^= 1;
|
|
assert_ne!(hash, envelope.sha256());
|
|
let error = key.decrypt(&envelope).unwrap_err().to_string();
|
|
assert!(!error.contains(std::str::from_utf8(expected)?));
|
|
Ok(())
|
|
}
|