Skip to main content

crev_lib/
id.rs

1//! `LockedId` is for you, the local crev user. `Id` is for identifying other users.
2
3use 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
16/// Callback to read the password
17pub type PassphraseFn<'a> = &'a dyn Fn() -> std::io::Result<String>;
18
19/// Stored in your config to know how to hash your passphrase
20#[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/// Serialized, stored on disk
33#[derive(Serialize, Deserialize, Debug, Clone)]
34pub struct LockedId {
35    version: i64,
36
37    /// Where your crev-proofs git repo is
38    #[serde(flatten)]
39    pub url: Option<crev_data::Url>,
40
41    /// This is used in `PublicId` to identify users
42    #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")]
43    #[serde(rename = "public-key")]
44    pub public_key: Vec<u8>,
45
46    /// Needs passphrase
47    #[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    /// Somewhat surprisingly, you get full YAML of the file
61    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
66/// Parses YAML file
67impl 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    /// Encrypt and throw away the key
77    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    /// Extract only the public identity part from all data. Useful for displaying user's identity.
135    #[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    /// Write the Id to this file, overwriting it
147    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    /// Decrypt
162    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    /// Used for temporary/default identity, but obviously not very secure to store
231    #[must_use]
232    pub fn has_no_passphrase(&self) -> bool {
233        self.passphrase_config.iterations == 1 && self.to_unlocked("").is_ok()
234    }
235
236    /// Config for empty passphrase. User chose no security, so they're getting none.
237    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}