//! 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(()) }