1use crate::{Error, Result};
4use aes_siv::KeyInit;
5use argon2::{self, Config};
6use crev_common::{
7 rand::random_vec,
8 serde::{as_base64, from_base64},
9};
10use crev_data::id::{PublicId, UnlockedId};
11use serde::{Deserialize, Serialize};
12use std::{self, fmt, io::BufReader, path::Path};
13
14const CURRENT_LOCKED_ID_SERIALIZATION_VERSION: i64 = -1;
15
16pub type PassphraseFn<'a> = &'a dyn Fn() -> std::io::Result<String>;
18
19#[derive(Serialize, Deserialize, Debug, Clone)]
21pub struct PassphraseConfig {
22 version: u32,
23 variant: String,
24 iterations: u32,
25 #[serde(rename = "memory-size")]
26 memory_size: u32,
27 lanes: Option<u32>,
28 #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")]
29 salt: Vec<u8>,
30}
31
32#[derive(Serialize, Deserialize, Debug, Clone)]
34pub struct LockedId {
35 version: i64,
36
37 #[serde(flatten)]
39 pub url: Option<crev_data::Url>,
40
41 #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")]
43 #[serde(rename = "public-key")]
44 pub public_key: Vec<u8>,
45
46 #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")]
48 #[serde(rename = "sealed-secret-key")]
49 sealed_secret_key: Vec<u8>,
50
51 #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")]
52 #[serde(rename = "seal-nonce")]
53 seal_nonce: Vec<u8>,
54
55 #[serde(rename = "pass")]
56 passphrase_config: PassphraseConfig,
57}
58
59impl fmt::Display for LockedId {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str(&serde_yaml::to_string(self).map_err(|_| fmt::Error)?)
63 }
64}
65
66impl std::str::FromStr for LockedId {
68 type Err = serde_yaml::Error;
69
70 fn from_str(yaml_str: &str) -> std::result::Result<Self, Self::Err> {
71 serde_yaml::from_str::<LockedId>(yaml_str)
72 }
73}
74
75impl LockedId {
76 pub fn from_unlocked_id(unlocked_id: &UnlockedId, passphrase: &str) -> Result<LockedId> {
78 let config = if !passphrase.is_empty() {
79 Config {
80 variant: argon2::Variant::Argon2id,
81 version: argon2::Version::Version13,
82
83 hash_length: 64,
84 mem_cost: 4096,
85 time_cost: 192,
86
87 lanes: std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as u32,
88
89 ad: &[],
90 secret: &[],
91 }
92 } else {
93 Self::weak_passphrase_config()
94 };
95
96 let pwsalt = random_vec(32);
97 let pwhash =
98 argon2::hash_raw(passphrase.as_bytes(), &pwsalt, &config).map_err(Error::Passphrase)?;
99
100 let seal_nonce = random_vec(32);
101 let sealed_secret_key = {
102 use aes_siv::{aead::generic_array::GenericArray, siv::IV_SIZE};
103
104 let secret = unlocked_id.keypair.secret.as_bytes();
105 let mut siv = aes_siv::siv::Aes256Siv::new(&GenericArray::clone_from_slice(&pwhash));
106 let mut buffer = vec![0; IV_SIZE + secret.len()];
107 buffer[IV_SIZE..].copy_from_slice(secret);
108 let tag = siv
109 .encrypt_in_place_detached([&[] as &[u8], &seal_nonce], &mut buffer[IV_SIZE..])
110 .expect("aes-encrypt");
111 buffer[..IV_SIZE].copy_from_slice(&tag);
112 buffer
113 };
114
115 Ok(LockedId {
116 version: CURRENT_LOCKED_ID_SERIALIZATION_VERSION,
117 public_key: unlocked_id.keypair.public.to_bytes().to_vec(),
118 sealed_secret_key,
119 seal_nonce,
120 url: unlocked_id.url().cloned(),
121 passphrase_config: PassphraseConfig {
122 salt: pwsalt,
123 iterations: config.time_cost,
124 memory_size: config.mem_cost,
125 version: 0x13,
126 lanes: Some(config.lanes),
127 variant: config.variant.as_lowercase_str().to_string(),
128 },
129 })
130 }
131
132 #[must_use]
134 pub fn to_public_id(&self) -> PublicId {
135 PublicId::new_from_pubkey(self.public_key.clone(), self.url.clone())
136 .expect("Invalid locked id.")
137 }
138
139 #[must_use]
140 pub fn pub_key_as_base64(&self) -> String {
141 crev_common::base64_encode(&self.public_key)
142 }
143
144 pub fn save_to(&self, path: &Path) -> Result<()> {
146 let s = self.to_string();
147 crev_common::store_str_to_file(path, &s).map_err(|e| Error::FileWrite(e, path.into()))
148 }
149
150 pub fn read_from_yaml_file(path: &Path) -> Result<Self> {
151 let mut file = BufReader::new(
152 std::fs::File::open(path)
153 .map_err(|e| Error::IdLoadError(Box::new((path.into(), e))))?,
154 );
155
156 Ok(serde_yaml::from_reader(&mut file)?)
157 }
158
159 pub fn to_unlocked(&self, passphrase: &str) -> Result<UnlockedId> {
161 let LockedId {
162 ref version,
163 ref url,
164 ref public_key,
165 ref sealed_secret_key,
166 ref seal_nonce,
167 ref passphrase_config,
168 } = self;
169 {
170 if *version > CURRENT_LOCKED_ID_SERIALIZATION_VERSION {
171 return Err(Error::UnsupportedVersion(*version));
172 }
173 let mut config = Config {
174 variant: argon2::Variant::from_str(&passphrase_config.variant)?,
175 version: argon2::Version::Version13,
176
177 hash_length: 64,
178 mem_cost: passphrase_config.memory_size,
179 time_cost: passphrase_config.iterations,
180
181 lanes: std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as u32,
182
183 ad: &[],
184 secret: &[],
185 };
186
187 if let Some(lanes) = passphrase_config.lanes {
188 config.lanes = lanes;
189 } else {
190 log::error!(
191 "`lanes` not configured. Old bug. See: https://github.com/crev-dev/cargo-crev/issues/151"
192 );
193 log::info!("Using `lanes: {}`", config.lanes);
194 }
195
196 let passphrase_hash =
197 argon2::hash_raw(passphrase.as_bytes(), &passphrase_config.salt, &config)?;
198
199 let secret_key = {
200 use aes_siv::{aead::generic_array::GenericArray, siv::IV_SIZE, Tag};
201
202 let mut siv =
203 aes_siv::siv::Aes256Siv::new(&GenericArray::clone_from_slice(&passphrase_hash));
204 let mut buffer = sealed_secret_key.clone();
205 let tag = Tag::clone_from_slice(&buffer[..IV_SIZE]);
206 siv.decrypt_in_place_detached(
207 [&[] as &[u8], seal_nonce],
208 &mut buffer[IV_SIZE..],
209 &tag,
210 )
211 .map_err(|_| Error::IncorrectPassphrase)?;
212 buffer.drain(..IV_SIZE);
213 buffer
214 };
215
216 assert!(!secret_key.is_empty());
217
218 let result = UnlockedId::new(url.clone(), &secret_key)?;
219 if public_key != &result.keypair.public.to_bytes() {
220 return Err(Error::PubKeyMismatch);
221 }
222 Ok(result)
223 }
224 }
225
226 #[must_use]
228 pub fn has_no_passphrase(&self) -> bool {
229 self.passphrase_config.iterations == 1 && self.to_unlocked("").is_ok()
230 }
231
232 fn weak_passphrase_config() -> Config<'static> {
234 Config {
235 variant: argon2::Variant::Argon2id,
236 version: argon2::Version::Version13,
237
238 hash_length: 64,
239 mem_cost: 16,
240 time_cost: 1,
241
242 lanes: 1,
243
244 ad: &[],
245 secret: &[],
246 }
247 }
248}