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()
88 .map(|n| n.get())
89 .unwrap_or(1) as u32,
90
91 ad: &[],
92 secret: &[],
93 }
94 } else {
95 Self::weak_passphrase_config()
96 };
97
98 let pwsalt = random_vec(32);
99 let pwhash =
100 argon2::hash_raw(passphrase.as_bytes(), &pwsalt, &config).map_err(Error::Passphrase)?;
101
102 let seal_nonce = random_vec(32);
103 let sealed_secret_key = {
104 use aes_siv::{aead::generic_array::GenericArray, siv::IV_SIZE};
105
106 let secret = unlocked_id.keypair.secret.as_bytes();
107 let mut siv = aes_siv::siv::Aes256Siv::new(&GenericArray::clone_from_slice(&pwhash));
108 let mut buffer = vec![0; IV_SIZE + secret.len()];
109 buffer[IV_SIZE..].copy_from_slice(secret);
110 let tag = siv
111 .encrypt_in_place_detached([&[] as &[u8], &seal_nonce], &mut buffer[IV_SIZE..])
112 .expect("aes-encrypt");
113 buffer[..IV_SIZE].copy_from_slice(&tag);
114 buffer
115 };
116
117 Ok(LockedId {
118 version: CURRENT_LOCKED_ID_SERIALIZATION_VERSION,
119 public_key: unlocked_id.keypair.public.to_bytes().to_vec(),
120 sealed_secret_key,
121 seal_nonce,
122 url: unlocked_id.url().cloned(),
123 passphrase_config: PassphraseConfig {
124 salt: pwsalt,
125 iterations: config.time_cost,
126 memory_size: config.mem_cost,
127 version: 0x13,
128 lanes: Some(config.lanes),
129 variant: config.variant.as_lowercase_str().to_string(),
130 },
131 })
132 }
133
134 #[must_use]
136 pub fn to_public_id(&self) -> PublicId {
137 PublicId::new_from_pubkey(self.public_key.clone(), self.url.clone())
138 .expect("Invalid locked id.")
139 }
140
141 #[must_use]
142 pub fn pub_key_as_base64(&self) -> String {
143 crev_common::base64_encode(&self.public_key)
144 }
145
146 pub fn save_to(&self, path: &Path) -> Result<()> {
148 let s = self.to_string();
149 crev_common::store_str_to_file(path, &s).map_err(|e| Error::FileWrite(e, path.into()))
150 }
151
152 pub fn read_from_yaml_file(path: &Path) -> Result<Self> {
153 let mut file = BufReader::new(
154 std::fs::File::open(path)
155 .map_err(|e| Error::IdLoadError(Box::new((path.into(), e))))?,
156 );
157
158 Ok(serde_yaml::from_reader(&mut file)?)
159 }
160
161 pub fn to_unlocked(&self, passphrase: &str) -> Result<UnlockedId> {
163 let LockedId {
164 version,
165 url,
166 public_key,
167 sealed_secret_key,
168 seal_nonce,
169 passphrase_config,
170 } = self;
171 {
172 if *version > CURRENT_LOCKED_ID_SERIALIZATION_VERSION {
173 return Err(Error::UnsupportedVersion(*version));
174 }
175 let mut config = Config {
176 variant: argon2::Variant::from_str(&passphrase_config.variant)?,
177 version: argon2::Version::Version13,
178
179 hash_length: 64,
180 mem_cost: passphrase_config.memory_size,
181 time_cost: passphrase_config.iterations,
182
183 lanes: std::thread::available_parallelism()
184 .map(|n| n.get())
185 .unwrap_or(1) as u32,
186
187 ad: &[],
188 secret: &[],
189 };
190
191 if let Some(lanes) = passphrase_config.lanes {
192 config.lanes = lanes;
193 } else {
194 log::error!(
195 "`lanes` not configured. Old bug. See: https://github.com/crev-dev/cargo-crev/issues/151"
196 );
197 log::info!("Using `lanes: {}`", config.lanes);
198 }
199
200 let passphrase_hash =
201 argon2::hash_raw(passphrase.as_bytes(), &passphrase_config.salt, &config)?;
202
203 let secret_key = {
204 use aes_siv::{Tag, aead::generic_array::GenericArray, siv::IV_SIZE};
205
206 let mut siv =
207 aes_siv::siv::Aes256Siv::new(&GenericArray::clone_from_slice(&passphrase_hash));
208 let mut buffer = sealed_secret_key.clone();
209 let tag = Tag::clone_from_slice(&buffer[..IV_SIZE]);
210 siv.decrypt_in_place_detached(
211 [&[] as &[u8], seal_nonce],
212 &mut buffer[IV_SIZE..],
213 &tag,
214 )
215 .map_err(|_| Error::IncorrectPassphrase)?;
216 buffer.drain(..IV_SIZE);
217 buffer
218 };
219
220 assert!(!secret_key.is_empty());
221
222 let result = UnlockedId::new(url.clone(), &secret_key)?;
223 if public_key != &result.keypair.public.to_bytes() {
224 return Err(Error::PubKeyMismatch);
225 }
226 Ok(result)
227 }
228 }
229
230 #[must_use]
232 pub fn has_no_passphrase(&self) -> bool {
233 self.passphrase_config.iterations == 1 && self.to_unlocked("").is_ok()
234 }
235
236 fn weak_passphrase_config() -> Config<'static> {
238 Config {
239 variant: argon2::Variant::Argon2id,
240 version: argon2::Version::Version13,
241
242 hash_length: 64,
243 mem_cost: 16,
244 time_cost: 1,
245
246 lanes: 1,
247
248 ad: &[],
249 secret: &[],
250 }
251 }
252}