diff --git a/crates/relicario-core/src/item_types/totp.rs b/crates/relicario-core/src/item_types/totp.rs index 00e7b80..74d4d4f 100644 --- a/crates/relicario-core/src/item_types/totp.rs +++ b/crates/relicario-core/src/item_types/totp.rs @@ -10,9 +10,9 @@ use crate::error::{RelicarioError, Result}; /// Steam Mobile Authenticator's 5-character output alphabet. /// Deliberately excludes ambiguous glyphs (0/O, 1/I/L, S/5, A/Z). -// -// Not RFC 4648 — Steam Guard's de-ambiguated alphabet; see `crate::base32` -// for the standard implementation. +/// +/// Not RFC 4648 — Steam Guard's de-ambiguated alphabet; see [`crate::base32`] +/// for the standard implementation. const STEAM_ALPHABET: &[u8] = b"23456789BCDFGHJKMNPQRTVWXY"; #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/crates/relicario-core/src/lib.rs b/crates/relicario-core/src/lib.rs index dbdc33a..237a2d1 100644 --- a/crates/relicario-core/src/lib.rs +++ b/crates/relicario-core/src/lib.rs @@ -14,6 +14,8 @@ //! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and //! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02. //! - [`ids`] — `ItemId`, `FieldId`, and content-addressed `AttachmentId`. +//! - [`base32`] — RFC 4648 base32 codec used for TOTP secret encode/decode. +//! - [`mime`] — Filename-extension → MIME-type guess for attachment storage. //! - [`time`] — unix-seconds + `MonthYear` for card expiries. //! - [`item_types`] — Per-type cores (`LoginCore`, `SecureNoteCore`, etc.) and the //! `ItemCore`/`ItemType` enums. diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs index 66825e7..f6c6b80 100644 --- a/crates/relicario-wasm/src/lib.rs +++ b/crates/relicario-wasm/src/lib.rs @@ -330,6 +330,32 @@ pub fn embed_image_secret(carrier: &[u8], secret: &[u8]) -> Result, JsEr imgsecret::embed(carrier, s).map_err(|e| JsError::new(&e.to_string())) } +// ── Pure parsers (no session needed) ──────────────────────────────────────── + +use relicario_core::{base32 as core_base32, mime as core_mime, MonthYear}; + +/// Parse a card-expiry string (`MM/YYYY` / `MM-YYYY` / `MM/YY`). +/// Returns a plain `{ month, year }` object on success. +#[wasm_bindgen] +pub fn parse_month_year(s: &str) -> Result { + let my = MonthYear::parse(s).map_err(|e| JsError::new(&e.to_string()))?; + js_value_for(&my) +} + +/// Decode an RFC 4648 base32 string (case-insensitive, optional padding, +/// whitespace-stripped). Returned as `Uint8Array` on the JS side. +#[wasm_bindgen] +pub fn base32_decode_lenient(s: &str) -> Result, JsError> { + core_base32::decode_rfc4648_lenient(s).map_err(|e| JsError::new(&e.to_string())) +} + +/// Guess a MIME type from a filename's extension. Returns +/// `application/octet-stream` for unknown or missing extensions. +#[wasm_bindgen] +pub fn guess_mime(filename: &str) -> String { + core_mime::guess_for_extension(filename).to_string() +} + use relicario_core::item_types::{TotpConfig, compute_totp_code}; #[wasm_bindgen] @@ -624,4 +650,24 @@ mod session_tests { // Should fail with a header validation error. assert!(err.is_err()); } + + #[test] + fn base32_decode_lenient_round_trips_known_vector() { + let bytes = super::base32_decode_lenient("MZXW6YTBOI").unwrap(); + assert_eq!(bytes, b"foobar"); + } + + #[test] + fn guess_mime_known_and_unknown_extensions() { + assert_eq!(super::guess_mime("doc.pdf"), "application/pdf"); + assert_eq!(super::guess_mime("photo.JPEG"), "image/jpeg"); + assert_eq!(super::guess_mime("file.xyz"), "application/octet-stream"); + } + + // Error paths and JsValue serialization can't be exercised natively — + // JsError::new and serde_wasm_bindgen::Serializer call wasm-bindgen + // imports that panic off-wasm (same constraint as + // `parse_lastpass_csv_json_propagates_header_errors` above). Those + // paths are covered in core: `time::tests::parse_rejects_malformed` + // and `base32::tests::decode_rfc4648_lenient_rejects_non_alphabet_chars`. } diff --git a/extension/src/wasm.d.ts b/extension/src/wasm.d.ts index 3643996..2553999 100644 --- a/extension/src/wasm.d.ts +++ b/extension/src/wasm.d.ts @@ -59,6 +59,11 @@ declare module 'relicario-wasm' { export function extract_image_secret(image_bytes: Uint8Array): Uint8Array; export function embed_image_secret(carrier: Uint8Array, secret: Uint8Array): Uint8Array; + // Pure parsers (no session needed) + export function parse_month_year(s: string): { month: number; year: number }; + export function base32_decode_lenient(s: string): Uint8Array; + export function guess_mime(filename: string): string; + export function totp_compute(config_json: string, now_unix_seconds: bigint): TotpCode; export function register_device(name: string): {