nethsm_backup/lib.rs
1//! # NetHSM backup
2//!
3//! A library to parse, decrypt, validate and browse NetHSM backups.
4//!
5//! ## Format
6//!
7//! The backup format is an [internal detail of the NetHSM][INT].
8//! This library implements the version `0` format which should be supported even on newer devices.
9//!
10//! The backup file consists of two formats: one outer, which contains unencrypted magic values and
11//! framing for the inner format. The inner format can be accessed after decrypting values within
12//! the outer one. Both of them are using similar primitives such as length-prefixed byte vectors.
13//!
14//! Length-prefixed byte vectors are always encoded as 3 big-endian length bytes followed by the
15//! given number of bytes.
16//!
17//! ### Outer format
18//!
19//! The outer format contains, in order, the header:
20//!
21//! - magic value: 15 bytes consisting of: `_NETHSM_BACKUP_`,
22//! - version tag: 1 byte, currently there's only one version which is stored as a `NUL` byte
23//! (`0x00`).
24//!
25//! and several length-prefixed values:
26//!
27//! - salt,
28//! - encrypted inner version,
29//! - encrypted domain key,
30//! - variable number of encrypted items.
31//!
32//! ### Inner format
33//!
34//! The inner format is accessed by decrypting the outer format.
35//! The decryption key is derived using [scrypt] based on the passphrase provided by the user and
36//! the salt contained in the outer format.
37//!
38//! The following values exist in the inner format:
39//!
40//! - version: inner format version, the only known value is `0x00`, this is retrieved by decrypting
41//! encrypted inner version with the `backup-version` associated additional data,
42//! - domain key: decrypted inner domain key with `domain-key` as AAD,
43//! - items: decrypted key/values with the `backup` AAD, they are stored as a length-prefixed string
44//! for a key and a value which is stored as a rest of the decrypted value.
45//!
46//! Sample list of inner format keys:
47//!
48//! - `/.initialized`
49//! - `/authentication/.version`
50//! - `/authentication/admin`
51//! - `/authentication/backup1`
52//! - `/authentication/encoperator1`
53//! - `/authentication/metrics1`
54//! - `/authentication/namespace1~admin`
55//! - `/authentication/namespace1~operator`
56//! - `/authentication/namespace2~admin`
57//! - `/authentication/namespace2~operator`
58//! - `/authentication/operator1`
59//! - `/authentication/operator2`
60//! - `/config/backup-key`
61//! - `/config/backup-salt`
62//! - `/config/certificate`
63//! - `/config/private-key`
64//! - `/config/time-offset`
65//! - `/config/unlock-salt`
66//! - `/config/version`
67//! - `/domain-key/attended`
68//! - `/key/.version`
69//! - `/namespace/.version`
70//! - `/namespace/namespace1`
71//! - `/namespace/namespace2`
72//!
73//! A fresh list of values in a backup can be generated by running the integration test: `cargo test
74//! -- --ignored --nocapture create_backup_and_decrypt_it`
75//!
76//! ## Examples
77//!
78//! Listing all fields in a backup file:
79//!
80//! ```no_run
81//! # fn main() -> testresult::TestResult {
82//! use std::collections::HashMap;
83//!
84//! use nethsm_backup::Backup;
85//!
86//! let backup = Backup::parse(std::fs::File::open("tests/nethsm.backup-file.bkp")?)?;
87//! let decryptor = backup.decrypt(b"my-very-unsafe-backup-passphrase")?;
88//!
89//! assert_eq!(decryptor.version()?, [0]);
90//!
91//! for item in decryptor.items_iter() {
92//! let (key, value) = item?;
93//! println!("Found {key} with value: {value:X?}");
94//! }
95//! # Ok(()) }
96//! ```
97//!
98//! Dumping the value of one specified field (here `/config/version`):
99//!
100//! ```no_run
101//! # fn main() -> testresult::TestResult {
102//! use std::collections::HashMap;
103//!
104//! use nethsm_backup::Backup;
105//!
106//! let backup = Backup::parse(std::fs::File::open("tests/nethsm.backup-file.bkp")?)?;
107//! let decryptor = backup.decrypt(b"my-very-unsafe-backup-passphrase")?;
108//!
109//! assert_eq!(decryptor.version()?, [0]);
110//!
111//! for (key, value) in decryptor
112//! .items_iter()
113//! .flat_map(|item| item.ok())
114//! .filter(|(key, _)| key == "/config/version")
115//! {
116//! println!("Found {key} with value: {value:X?}");
117//! }
118//! # Ok(()) }
119//! ```
120//!
121//! [INT]: https://github.com/Nitrokey/nethsm-sdk-rs/issues/36#issuecomment-2504592259
122//! [scrypt]: https://docs.rs/scrypt
123
124use std::{
125 io::{ErrorKind, Read},
126 slice::Iter,
127};
128
129use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _};
130use log::error;
131use scrypt::{Params, scrypt};
132
133/// Backup processing error.
134#[derive(Debug, thiserror::Error)]
135#[non_exhaustive]
136pub enum Error {
137 /// I/O error.
138 #[error("I/O error: {0}")]
139 Io(#[from] std::io::Error),
140
141 /// Invalid parameters to the Scrypt key derivation.
142 #[error("Invalid Scrypt key derivation parameters")]
143 InvalidScryptParams,
144
145 /// Scrypt key derviation failed.
146 #[error("Scrypt key derivation failed")]
147 ScryptKeyDerivation,
148
149 /// AES-GCM decryption error.
150 #[error("AES-GCM decryption error")]
151 Decryption,
152
153 /// Unicode decode error.
154 #[error("Key is not a valid UTF-8: {0}")]
155 Utf8(#[from] std::string::FromUtf8Error),
156
157 /// Magic value is incorrect.
158 ///
159 /// This file is either corrupted or not a NetHSM backup.
160 #[error("Bad magic value: {0:X?}")]
161 BadMagic(Vec<u8>),
162
163 /// Version number is not recognized.
164 ///
165 /// This library supports only version `0` backups.
166 #[error(
167 "Unsupported backup version number: {backup_version:?}. The highest supported version is {highest_supported_version}"
168 )]
169 BadVersion {
170 /// The highest version that is supported by the current implementation..
171 highest_supported_version: u8,
172
173 /// The version of the backup file.
174 backup_version: Vec<u8>,
175 },
176}
177
178/// Custom [`Result`] wrapper for [`Error`]s that may occur when using `nethsm_backup`.
179pub type Result<T> = std::result::Result<T, Error>;
180
181/// Magic value that is contained in all NetHSM backups.
182const MAGIC: &[u8] = b"_NETHSM_BACKUP_";
183
184/// Read 3 bytes from the provided reader and interprets it as a [usize].
185fn read_usize(reader: &mut impl Read) -> std::io::Result<usize> {
186 const LEN: usize = size_of::<usize>();
187 let mut bytes = [0; LEN];
188 // read exactly 3 bytes
189 reader.read_exact(&mut bytes[LEN - 3..])?;
190 Ok(usize::from_be_bytes(bytes))
191}
192
193/// Read a byte vector from the underlying reader.
194///
195/// A byte vector is always stored as a [usize] (see [read_usize]) and
196/// then a number of bytes.
197fn read_field(reader: &mut impl Read) -> Result<Vec<u8>> {
198 let len = read_usize(reader)?;
199 let mut field = vec![0; len];
200 reader.read_exact(&mut field)?;
201 Ok(field)
202}
203
204/// Check if the reader contains correct [MAGIC] value.
205///
206/// # Errors
207///
208/// Returns:
209/// * [Error::BadMagic] if an unrecognized magic value is found.
210/// * [Error::Io] if an I/O error occurs.
211fn check_magic(reader: &mut impl Read) -> Result<()> {
212 let mut magic = [0; MAGIC.len()];
213 reader.read_exact(&mut magic)?;
214 if MAGIC != magic {
215 return Err(Error::BadMagic(magic.into()));
216 }
217 Ok(())
218}
219
220/// Check if the reader contains version number that is understood.
221///
222/// # Errors
223///
224/// Returns:
225/// * [Error::BadVersion] if an unrecognized version value is found.
226/// * [Error::Io] if an I/O error occurs.
227fn check_version(reader: &mut impl Read) -> Result<()> {
228 let mut version = [0; 1];
229 reader.read_exact(&mut version)?;
230 let version = version[0];
231 if version != 0 {
232 return Err(Error::BadVersion {
233 highest_supported_version: 0,
234 backup_version: vec![version],
235 });
236 }
237 Ok(())
238}
239
240/// Data of a NetHSM backup.
241///
242/// This object contains the data of a successfully parsed and well-formed NetHSM backup.
243#[derive(Debug)]
244pub struct Backup {
245 salt: Vec<u8>,
246 encrypted_version: Vec<u8>,
247 encrypted_domain_key: Vec<u8>,
248 items: Vec<Vec<u8>>,
249}
250
251impl Backup {
252 /// Parse the backup from a reader.
253 ///
254 /// The reader must contain a well-formed, valid NetHSM backup file.
255 ///
256 /// # Errors
257 ///
258 /// Returns:
259 /// * [Error::BadVersion] if an unrecognized version value is found.
260 /// * [Error::BadMagic] if an unrecognized version value is found.
261 /// * [Error::Io] if an I/O error occurs when reading the backup.
262 pub fn parse(mut reader: impl Read) -> Result<Self> {
263 check_magic(&mut reader)?;
264 check_version(&mut reader)?;
265
266 let salt = read_field(&mut reader)?;
267 let encrypted_version = read_field(&mut reader)?;
268 let encrypted_domain_key = read_field(&mut reader)?;
269
270 let mut items = vec![];
271 loop {
272 match read_usize(&mut reader) {
273 Ok(len) => {
274 let mut field = vec![0; len];
275 reader.read_exact(&mut field)?;
276 items.push(field);
277 }
278 Err(error) if error.kind() == ErrorKind::UnexpectedEof => {
279 break;
280 }
281 Err(error) => {
282 return Err(error)?;
283 }
284 }
285 }
286
287 Ok(Self {
288 salt,
289 encrypted_version,
290 encrypted_domain_key,
291 items,
292 })
293 }
294
295 /// Create a [`BackupDecryptor`] that will decrypt items with the provided passphrase.
296 ///
297 /// # Errors
298 ///
299 /// Even though this function returns a `Result` it is unlikely to fail since all parameters are
300 /// static.
301 pub fn decrypt(&self, passphrase: &[u8]) -> Result<BackupDecryptor> {
302 BackupDecryptor::new(self, passphrase)
303 }
304}
305
306/// Backup decryptor which decrypts backup items on the fly.
307pub struct BackupDecryptor<'a> {
308 backup: &'a Backup,
309 cipher: Aes256Gcm,
310}
311
312impl std::fmt::Debug for BackupDecryptor<'_> {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 f.debug_struct("BackupDecryptor")
315 .field("backup", self.backup)
316 .field("cipher", &"[REDACTED]")
317 .finish()
318 }
319}
320
321impl<'a> BackupDecryptor<'a> {
322 /// Create a new [`BackupDecryptor`] using a [`Backup`] and a passphrase.
323 ///
324 /// # Errors
325 ///
326 /// Even though this function returns a `Result` it is unlikely to fail since all parameters are
327 /// static.
328 fn new(backup: &'a Backup, passphrase: &[u8]) -> Result<Self> {
329 let mut key = [0; 32];
330 scrypt(
331 passphrase,
332 &backup.salt,
333 &Params::new(14, 8, 16, 32).map_err(|e| {
334 error!("Scrypt parameters are invalid. This should not happen as they are statically chosen: {e:?}");
335 Error::InvalidScryptParams
336 })?,
337 &mut key,
338 )
339 .map_err(|e| {
340 error!("Scrypt derivation failed: {e:?}");
341 Error::ScryptKeyDerivation
342 })?;
343 let cipher = Aes256Gcm::new(&key.into());
344 Ok(Self { backup, cipher })
345 }
346
347 /// Decrypts `ciphertext` while verifying additional data (`aad`).
348 ///
349 /// # Errors
350 ///
351 /// Returns:
352 /// * [Error::Decryption] if a decryption error is encountered, for example the ciphertext is of
353 /// incorrect length, has been tampered with, the decryption passphrase is wrong or the
354 /// additional authenticated data is incorrect.
355 fn decrypt(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
356 let Some((nonce, msg)) = ciphertext.split_at_checked(12) else {
357 return Err(Error::Decryption);
358 };
359
360 let payload = aes_gcm::aead::Payload { msg, aad };
361
362 let plaintext = self.cipher.decrypt(nonce.into(), payload).map_err(|e| {
363 error!("Decryption failed: {e:?}");
364 Error::Decryption
365 })?;
366 Ok(plaintext)
367 }
368
369 /// Decrypted backup version.
370 ///
371 /// # Errors
372 ///
373 /// Returns:
374 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted
375 /// version is of incorrect length, has been tampered with, the decryption passphrase is wrong
376 /// or the additional authenticated data is incorrect (e.g. a different encrypted piece of
377 /// data is impersonating the backup version).
378 pub fn version(&self) -> Result<Vec<u8>> {
379 self.decrypt(&self.backup.encrypted_version, b"backup-version")
380 }
381
382 /// Decrypted domain key.
383 ///
384 /// # Errors
385 ///
386 /// Returns:
387 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted domain
388 /// key is of incorrect length, has been tampered with, the decryption passphrase is wrong or
389 /// the additional authenticated data is incorrect (e.g. a different encrypted piece of data
390 /// is impersonating the domain key).
391 pub fn domain_key(&self) -> Result<Vec<u8>> {
392 self.decrypt(&self.backup.encrypted_domain_key, b"domain-key")
393 }
394
395 /// Returns an iterator over backup entries.
396 ///
397 /// The entries are pairs of keys (which are strings) and values (byte vectors).
398 /// Since the entries are decrypted as they are being read the pairs are wrapped in
399 /// [`Result`]s.
400 ///
401 /// # Errors
402 ///
403 /// This function does not fail but reading the inner iterator may return errors:
404 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted entry
405 /// is of incorrect length, has been tampered with, the decryption passphrase is wrong or the
406 /// additional authenticated data is incorrect (e.g. a different encrypted piece of data is
407 /// impersonating the backup entry).
408 /// * [Error::Utf8] if the entry's key is not a well-formed UTF-8 string.
409 pub fn items_iter(&'a self) -> impl Iterator<Item = Result<(String, Vec<u8>)>> + 'a {
410 BackupItemDecryptor {
411 decryptor: self,
412 inner: self.backup.items.iter(),
413 }
414 }
415}
416
417/// Iterates over the entries of a backup and decrypts them on the fly.
418///
419/// This struct is a wrapper over an iterator of items of a [`Backup`].
420/// It keeps a state of the current item being processed.
421/// Each item is decrypted and then split into a UTF-8 string key and a value that is a byte vector.
422struct BackupItemDecryptor<'a> {
423 decryptor: &'a BackupDecryptor<'a>,
424 inner: Iter<'a, Vec<u8>>,
425}
426
427impl Iterator for BackupItemDecryptor<'_> {
428 type Item = Result<(String, Vec<u8>)>;
429
430 /// Return next pair of key and value.
431 ///
432 /// # Errors
433 ///
434 /// Returns
435 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted entry
436 /// is of incorrect length, has been tampered with, the decryption passphrase is wrong or the
437 /// additional authenticated data is incorrect (e.g. a different encrypted piece of data is
438 /// impersonating the backup entry).
439 /// * [Error::Utf8] if the entry's key is not a well-formed UTF-8 string.
440 fn next(&mut self) -> Option<Self::Item> {
441 self.inner.next().map(|item| {
442 let decrypted = self.decryptor.decrypt(item, b"backup")?;
443 let mut reader = std::io::Cursor::new(decrypted);
444 let key = String::from_utf8(read_field(&mut reader)?)?;
445 let mut value = vec![];
446 reader.read_to_end(&mut value)?;
447 Ok((key, value))
448 })
449 }
450}