Skip to main content

murk_cli/
types.rs

1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4use zeroize::Zeroizing;
5
6/// Current vault format version.
7pub const VAULT_VERSION: &str = "2.0";
8
9/// Default vault filename.
10pub const DEFAULT_VAULT_NAME: &str = ".murk";
11
12// -- Vault (on-disk format, v2) --
13// The entire .murk file is a single JSON document with per-value encryption.
14// Key names and schema are plaintext. Values are individually age-encrypted.
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Vault {
18    pub version: String,
19    pub created: String,
20    pub vault_name: String,
21    /// Repository URL, auto-detected from git remote during init.
22    #[serde(default, skip_serializing_if = "String::is_empty")]
23    pub repo: String,
24    /// Public keys only — no names. Name mappings live in the encrypted meta blob.
25    pub recipients: Vec<String>,
26    /// Key metadata — public, readable without decryption.
27    pub schema: BTreeMap<String, SchemaEntry>,
28    /// Per-value encrypted secrets. Each value is a separate age ciphertext.
29    pub secrets: BTreeMap<String, SecretEntry>,
30    /// Encrypted metadata blob: recipient names and integrity MAC.
31    pub meta: String,
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
35pub struct SchemaEntry {
36    pub description: String,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub example: Option<String>,
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub tags: Vec<String>,
41    /// When the key was first added.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub created: Option<String>,
44    /// When the value was last updated.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub updated: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct SecretEntry {
51    /// Shared value encrypted to all recipients.
52    pub shared: String,
53    /// Scoped overrides: pubkey → encrypted value (encrypted to that pubkey only).
54    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
55    pub scoped: BTreeMap<String, String>,
56}
57
58// -- Meta (encrypted, stored in vault.meta) --
59// Contains metadata only visible to recipients.
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Meta {
63    /// Maps pubkey → display name. The only place names are stored.
64    pub recipients: HashMap<String, String>,
65    /// Integrity MAC over secrets + schema.
66    pub mac: String,
67    /// BLAKE3 keyed MAC key (hex-encoded, 32 bytes). Generated at init, stored encrypted.
68    #[serde(default, skip_serializing_if = "Option::is_none", alias = "hmac_key")]
69    pub mac_key: Option<String>,
70    /// Pinned GitHub key fingerprints: username → [SHA256:...].
71    /// Used for TOFU (Trust On First Use) verification on `authorize github:user`.
72    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
73    pub github_pins: HashMap<String, Vec<String>>,
74}
75
76// -- Murk (decrypted in-memory state) --
77// The working representation after decryption. Commands read/modify this,
78// then save_vault compares against the original to minimize re-encryption.
79
80#[derive(Debug, Clone)]
81pub struct Murk {
82    /// Decrypted shared values. Wrapped in `Zeroizing` so plaintext is cleared
83    /// from memory when the `Murk` is dropped.
84    pub values: HashMap<String, Zeroizing<String>>,
85    /// Pubkey → display name (from meta).
86    pub recipients: HashMap<String, String>,
87    /// Scoped overrides: key → { pubkey → decrypted value }.
88    /// Only contains entries decryptable by the current identity.
89    pub scoped: HashMap<String, HashMap<String, Zeroizing<String>>>,
90    /// True if the vault uses a legacy unkeyed MAC (sha256/sha256v2).
91    pub legacy_mac: bool,
92    /// Pinned GitHub key fingerprints (carried from meta).
93    pub github_pins: HashMap<String, Vec<String>>,
94}