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}