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}