cyphr_cli/commands/
common.rs1use std::time::{SystemTime, UNIX_EPOCH};
7
8use base64ct::{Base64UrlUnpadded, Encoding};
9use coz::Thumbprint;
10use cyphr::Key;
11use cyphr_storage::{CommitEntry, FileStore, Genesis};
12
13use crate::Error;
14use crate::keystore::{JsonKeyStore, KeyStore, StoredKey};
15
16pub fn current_timestamp() -> i64 {
18 SystemTime::now()
19 .duration_since(UNIX_EPOCH)
20 .map(|d| d.as_secs() as i64)
21 .unwrap_or(0)
22}
23
24pub fn load_key_from_keystore(keystore: &JsonKeyStore, tmb: &str) -> crate::Result<Key> {
26 let stored = keystore.get(tmb)?;
27 let now = current_timestamp();
28
29 Ok(Key {
30 alg: stored.alg.clone(),
31 tmb: Thumbprint::from_bytes(decode_b64(tmb)?),
32 pub_key: stored.pub_key.clone(),
33 first_seen: now,
34 last_used: None,
35 revocation: None,
36 tag: stored.tag.clone(),
37 })
38}
39
40pub fn extract_genesis_from_commits(
53 commits: &[CommitEntry],
54 keystore: Option<&JsonKeyStore>,
55) -> crate::Result<Genesis> {
56 let first_commit = commits.first().ok_or(Error::MissingField("commits"))?;
57
58 if let Some(ks) = keystore {
61 if let Some(first_tx) = first_commit.cozies.first() {
62 if let Some(signer_tmb) = first_tx
63 .get("pay")
64 .and_then(|p| p.get("tmb"))
65 .and_then(|v| v.as_str())
66 {
67 if let Some(key_obj) = first_tx.get("key") {
69 if key_obj.get("tmb").and_then(|v| v.as_str()) == Some(signer_tmb) {
70 return extract_key_from_obj(key_obj).map(Genesis::Implicit);
71 }
72 }
73
74 if let Ok(key) = load_key_from_keystore(ks, signer_tmb) {
76 return Ok(Genesis::Implicit(key));
77 }
78 }
79 }
80 }
81
82 let mut genesis_keys = Vec::new();
84
85 for tx_value in &first_commit.cozies {
86 if let Some(key_obj) = tx_value.get("key") {
87 genesis_keys.push(extract_key_from_obj(key_obj)?);
88 }
89 }
90
91 if genesis_keys.is_empty() {
92 return Err(Error::Storage(
93 "cannot determine genesis keys from storage".into(),
94 ));
95 }
96
97 if genesis_keys.len() == 1 {
98 Ok(Genesis::Implicit(genesis_keys.remove(0)))
99 } else {
100 Ok(Genesis::Explicit(genesis_keys))
101 }
102}
103
104fn extract_key_from_obj(key_obj: &serde_json::Value) -> crate::Result<Key> {
106 let alg = key_obj
107 .get("alg")
108 .and_then(|v| v.as_str())
109 .ok_or(Error::MissingField("key.alg"))?;
110 let pub_b64 = key_obj
111 .get("pub")
112 .and_then(|v| v.as_str())
113 .ok_or(Error::MissingField("key.pub"))?;
114 let tmb_b64 = key_obj
115 .get("tmb")
116 .and_then(|v| v.as_str())
117 .ok_or(Error::MissingField("key.tmb"))?;
118
119 let pub_key = Base64UrlUnpadded::decode_vec(pub_b64)?;
120 let tmb_bytes = Base64UrlUnpadded::decode_vec(tmb_b64)?;
121
122 Ok(Key {
123 alg: alg.to_string(),
124 tmb: Thumbprint::from_bytes(tmb_bytes),
125 pub_key,
126 first_seen: 0,
127 last_used: None,
128 revocation: None,
129 tag: None,
130 })
131}
132
133pub fn parse_store(store_uri: &str) -> crate::Result<FileStore> {
135 if let Some(path) = store_uri.strip_prefix("file:") {
136 Ok(FileStore::new(path))
137 } else {
138 Err(Error::InvalidArgument(format!(
139 "unsupported store URI: {store_uri} (expected file:<path>)"
140 )))
141 }
142}
143
144pub fn parse_principal_genesis(s: &str) -> crate::Result<cyphr::PrincipalGenesis> {
146 let bytes = Base64UrlUnpadded::decode_vec(s)?;
147 Ok(cyphr::PrincipalGenesis::from_bytes(bytes))
148}
149
150pub fn decode_b64(s: &str) -> crate::Result<Vec<u8>> {
152 Ok(Base64UrlUnpadded::decode_vec(s)?)
153}
154
155pub fn generate_key(algo: &str, tag: Option<&str>) -> crate::Result<(String, StoredKey, Key)> {
160 let alg_enum = coz::Alg::from_str(algo)
161 .ok_or_else(|| Error::InvalidArgument(format!("unknown algorithm: {algo}")))?;
162
163 let keypair = alg_enum.generate_keypair();
164 let tmb_b64 = Base64UrlUnpadded::encode_string(keypair.thumbprint.as_bytes());
165
166 let now = current_timestamp();
167
168 let stored = StoredKey {
169 alg: algo.to_string(),
170 pub_key: keypair.pub_bytes.clone(),
171 prv_key: keypair.prv_bytes,
172 tag: tag.map(String::from),
173 };
174
175 let key = Key {
176 alg: algo.to_string(),
177 tmb: Thumbprint::from_bytes(keypair.thumbprint.as_bytes().to_vec()),
178 pub_key: keypair.pub_bytes,
179 first_seen: now,
180 last_used: None,
181 revocation: None,
182 tag: tag.map(String::from),
183 };
184
185 Ok((tmb_b64, stored, key))
186}