feat(core): key-file armor (relicario-keyfile-v1) encode/decode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
This commit is contained in:
73
crates/relicario-core/src/keyfile.rs
Normal file
73
crates/relicario-core/src/keyfile.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
//! Key-file armor for the pluggable second factor. The file holds the raw
|
||||||
|
//! 32-byte secret (base64) behind a version header -- it is the "something
|
||||||
|
//! you have", not an encrypted artifact (the passphrase is the other factor).
|
||||||
|
//!
|
||||||
|
//! The format is intentionally minimal and human-inspectable:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! relicario-keyfile-v1
|
||||||
|
//! <base64-standard of 32 bytes>
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Two lines, both terminated with `\n`. There is no encryption here: the file
|
||||||
|
//! is as sensitive as the raw secret itself. Store it accordingly (encrypted
|
||||||
|
//! drive, hardware token, offline backup). The passphrase remains the second
|
||||||
|
//! factor; together they feed Argon2id to derive the master key.
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use crate::error::{RelicarioError, Result};
|
||||||
|
|
||||||
|
const HEADER: &str = "relicario-keyfile-v1";
|
||||||
|
|
||||||
|
/// Encode a 32-byte secret into the `relicario-keyfile-v1` armor format.
|
||||||
|
///
|
||||||
|
/// Output is: `"relicario-keyfile-v1\n<base64>\n"` as UTF-8 bytes.
|
||||||
|
/// The returned bytes are suitable for writing directly to a `.relkey` file.
|
||||||
|
pub fn keyfile_encode(secret: &[u8; 32]) -> Vec<u8> {
|
||||||
|
format!("{HEADER}\n{}\n", STANDARD.encode(secret)).into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a `relicario-keyfile-v1` armored file back to the raw 32-byte secret.
|
||||||
|
///
|
||||||
|
/// Returns [`RelicarioError::Format`] if the header is missing or wrong, if the
|
||||||
|
/// body is not valid base64, or if the decoded length is not exactly 32 bytes.
|
||||||
|
pub fn keyfile_decode(bytes: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
|
||||||
|
let text = std::str::from_utf8(bytes)
|
||||||
|
.map_err(|_| RelicarioError::Format("key file is not UTF-8".into()))?;
|
||||||
|
let mut lines = text.lines();
|
||||||
|
if lines.next() != Some(HEADER) {
|
||||||
|
return Err(RelicarioError::Format("bad key-file header".into()));
|
||||||
|
}
|
||||||
|
let b64 = lines.next().unwrap_or("").trim();
|
||||||
|
let decoded = STANDARD
|
||||||
|
.decode(b64)
|
||||||
|
.map_err(|_| RelicarioError::Format("key-file body not base64".into()))?;
|
||||||
|
let arr: [u8; 32] = decoded
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| RelicarioError::Format("key-file secret must be 32 bytes".into()))?;
|
||||||
|
Ok(Zeroizing::new(arr))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn round_trip() {
|
||||||
|
let secret = [9u8; 32];
|
||||||
|
let armored = keyfile_encode(&secret);
|
||||||
|
assert!(std::str::from_utf8(&armored).unwrap().starts_with("relicario-keyfile-v1\n"));
|
||||||
|
let back = keyfile_decode(&armored).unwrap();
|
||||||
|
assert_eq!(*back, secret);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn rejects_bad_header() {
|
||||||
|
assert!(keyfile_decode(b"not-a-keyfile\nAAAA\n").is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn rejects_wrong_length() {
|
||||||
|
assert!(keyfile_decode(b"relicario-keyfile-v1\nAAAA\n").is_err()); // decodes to <32 bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,8 @@ pub use vault::{
|
|||||||
|
|
||||||
pub mod imgsecret;
|
pub mod imgsecret;
|
||||||
|
|
||||||
|
pub mod keyfile;
|
||||||
|
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub use backup::{pack_backup, unpack_backup, BackupInput, BackupOutput, BackupItem, BackupAttachment};
|
pub use backup::{pack_backup, unpack_backup, BackupInput, BackupOutput, BackupItem, BackupAttachment};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user