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().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    /// Extract only the public identity part from all data. Useful for displaying user's identity.
133    #[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    /// Write the Id to this file, overwriting it
145    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    /// Decrypt
160    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    /// Used for temporary/default identity, but obviously not very secure to store
227    #[must_use]
228    pub fn has_no_passphrase(&self) -> bool {
229        self.passphrase_config.iterations == 1 && self.to_unlocked("").is_ok()
230    }
231
232    /// Config for empty passphrase. User chose no security, so they're getting none.
233    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}