feat(wasm): attachment / generator / totp / imgsecret / id bridges
Also ports TOTP RFC 6238 compute to relicario-core::item_types::totp so native + CLI + WASM share one implementation (audit H5: CSPRNG via core's Uniform-sampling generator). Adds hmac = "0.12" and sha1 = "0.10" to relicario-core deps to support HOTP/TOTP HMAC with Sha1/Sha256/Sha512. RFC 6238 test vector (t=59, SHA-1, 8 digits) passes: "94287082".
This commit is contained in:
@@ -115,6 +115,123 @@ pub fn settings_encrypt(handle: &SessionHandle, settings_json: &str) -> Result<V
|
||||
.map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
// ── Task 20: attachment / generator / imgsecret / ID / TOTP bridges ─────────
|
||||
|
||||
use relicario_core::{decrypt_attachment, encrypt_attachment, FieldId, ItemId};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct EncryptedAttachment {
|
||||
aid: String,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl EncryptedAttachment {
|
||||
#[wasm_bindgen(getter)] pub fn aid(&self) -> String { self.aid.clone() }
|
||||
#[wasm_bindgen(getter)] pub fn bytes(&self) -> Vec<u8> { self.bytes.clone() }
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn attachment_encrypt(
|
||||
handle: &SessionHandle,
|
||||
plaintext: &[u8],
|
||||
max_bytes: u64,
|
||||
) -> Result<EncryptedAttachment, JsError> {
|
||||
need_key(handle)?;
|
||||
let enc = session::with(handle.0, |k| encrypt_attachment(plaintext, k, max_bytes))
|
||||
.unwrap()
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(EncryptedAttachment { aid: enc.id.as_str().to_owned(), bytes: enc.bytes })
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn attachment_decrypt(
|
||||
handle: &SessionHandle,
|
||||
encrypted: &[u8],
|
||||
) -> Result<Vec<u8>, JsError> {
|
||||
need_key(handle)?;
|
||||
let plain = session::with(handle.0, |k| decrypt_attachment(encrypted, k))
|
||||
.unwrap()
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(plain.to_vec())
|
||||
}
|
||||
|
||||
#[wasm_bindgen] pub fn new_item_id() -> String { ItemId::new().as_str().to_owned() }
|
||||
#[wasm_bindgen] pub fn new_field_id() -> String { FieldId::new().as_str().to_owned() }
|
||||
|
||||
use relicario_core::{
|
||||
generate_passphrase as core_generate_passphrase,
|
||||
generate_password as core_generate_password,
|
||||
rate_passphrase as core_rate_passphrase,
|
||||
GeneratorRequest,
|
||||
};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_password(request_json: &str) -> Result<String, JsError> {
|
||||
let req: GeneratorRequest = serde_json::from_str(request_json)
|
||||
.map_err(|e| JsError::new(&format!("generator request: {e}")))?;
|
||||
let out = core_generate_password(&req).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(out.as_str().to_owned())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_passphrase(request_json: &str) -> Result<String, JsError> {
|
||||
let req: GeneratorRequest = serde_json::from_str(request_json)
|
||||
.map_err(|e| JsError::new(&format!("generator request: {e}")))?;
|
||||
let out = core_generate_passphrase(&req).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(out.as_str().to_owned())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn rate_passphrase(p: &str) -> Result<JsValue, JsError> {
|
||||
let est = core_rate_passphrase(p);
|
||||
to_value(&serde_json::json!({
|
||||
"score": est.score,
|
||||
"guesses_log10": est.guesses_log10,
|
||||
})).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn extract_image_secret(image_bytes: &[u8]) -> Result<Vec<u8>, JsError> {
|
||||
let s = imgsecret::extract(image_bytes).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(s.to_vec())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn embed_image_secret(carrier: &[u8], secret: &[u8]) -> Result<Vec<u8>, JsError> {
|
||||
let s: &[u8; 32] = secret.try_into()
|
||||
.map_err(|_| JsError::new("secret must be exactly 32 bytes"))?;
|
||||
imgsecret::embed(carrier, s).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
use relicario_core::item_types::{TotpConfig, compute_totp_code};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct TotpCode {
|
||||
code: String,
|
||||
expires_at: u64,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl TotpCode {
|
||||
#[wasm_bindgen(getter)] pub fn code(&self) -> String { self.code.clone() }
|
||||
#[wasm_bindgen(getter)] pub fn expires_at(&self) -> u64 { self.expires_at }
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn totp_compute(
|
||||
config_json: &str,
|
||||
now_unix_seconds: u64,
|
||||
) -> Result<TotpCode, JsError> {
|
||||
let cfg: TotpConfig = serde_json::from_str(config_json)
|
||||
.map_err(|e| JsError::new(&format!("totp config: {e}")))?;
|
||||
let code = compute_totp_code(&cfg, now_unix_seconds)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
let period = cfg.period_seconds as u64;
|
||||
let expires_at = ((now_unix_seconds / period) + 1) * period;
|
||||
Ok(TotpCode { code, expires_at })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod session_tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user