feat(wasm): secure device API (private keys never cross to JS)
- register_device() generates signing + deploy keypairs via core device module, stores them in DEVICE_STATE (once_cell Lazy<Mutex>), and returns only public keys to JS - sign_for_git() signs data using the internal signing key - get_device_info() returns name and public keys; returns null if not registered - clear_device() zeroes and drops device state (logout / re-registration) - Removed generate_device_keypair() which exposed raw private key bytes Fixes audit I5: private key material no longer crosses the WASM boundary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
71
crates/relicario-wasm/src/device.rs
Normal file
71
crates/relicario-wasm/src/device.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! WASM device key management -- private keys never cross to JS.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use relicario_core::device as core_device;
|
||||
|
||||
/// In-memory device key storage (private keys held in WASM linear memory).
|
||||
static DEVICE_STATE: Lazy<Mutex<Option<DeviceState>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
struct DeviceState {
|
||||
name: String,
|
||||
signing_private: Zeroizing<String>,
|
||||
signing_public: String,
|
||||
/// Deploy key stored for future SSH git operations; not yet used for signing.
|
||||
#[allow(dead_code)]
|
||||
deploy_private: Zeroizing<String>,
|
||||
deploy_public: String,
|
||||
}
|
||||
|
||||
/// Register a new device, storing the keypairs internally and returning
|
||||
/// only the public keys. Private keys never leave WASM memory.
|
||||
pub fn register_device(name: &str) -> Result<(String, String), String> {
|
||||
let (signing_priv, signing_pub) =
|
||||
core_device::generate_keypair().map_err(|e| e.to_string())?;
|
||||
let (deploy_priv, deploy_pub) =
|
||||
core_device::generate_keypair().map_err(|e| e.to_string())?;
|
||||
|
||||
let state = DeviceState {
|
||||
name: name.to_string(),
|
||||
signing_private: signing_priv,
|
||||
signing_public: signing_pub.clone(),
|
||||
deploy_private: deploy_priv,
|
||||
deploy_public: deploy_pub.clone(),
|
||||
};
|
||||
|
||||
*DEVICE_STATE.lock().unwrap() = Some(state);
|
||||
|
||||
Ok((signing_pub, deploy_pub))
|
||||
}
|
||||
|
||||
/// Sign `data` using the registered device's signing key.
|
||||
/// Returns a base64-encoded signature.
|
||||
pub fn sign_for_git(data: &[u8]) -> Result<String, String> {
|
||||
let guard = DEVICE_STATE.lock().unwrap();
|
||||
let state = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "no device registered".to_string())?;
|
||||
|
||||
core_device::sign(&state.signing_private, data).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Return current device info: (name, signing_public_key, deploy_public_key).
|
||||
/// Returns None if no device has been registered in this session.
|
||||
pub fn get_device_info() -> Option<(String, String, String)> {
|
||||
let guard = DEVICE_STATE.lock().unwrap();
|
||||
guard.as_ref().map(|s| {
|
||||
(
|
||||
s.name.clone(),
|
||||
s.signing_public.clone(),
|
||||
s.deploy_public.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear device state (call on logout or before re-registration).
|
||||
pub fn clear_device() {
|
||||
*DEVICE_STATE.lock().unwrap() = None;
|
||||
}
|
||||
Reference in New Issue
Block a user