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//! --features _nethsm-integration-test -- --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};
132use signstar_crypto::passphrase::Passphrase;
133
134/// Backup processing error.
135#[derive(Debug, thiserror::Error)]
136#[non_exhaustive]
137pub enum Error {
138    /// I/O error.
139    #[error("I/O error: {0}")]
140    Io(#[from] std::io::Error),
141
142    /// Invalid parameters to the Scrypt key derivation.
143    #[error("Invalid Scrypt key derivation parameters")]
144    InvalidScryptParams,
145
146    /// Scrypt key derviation failed.
147    #[error("Scrypt key derivation failed")]
148    ScryptKeyDerivation,
149
150    /// AES-GCM decryption error.
151    #[error("AES-GCM decryption error")]
152    Decryption,
153
154    /// Unicode decode error.
155    #[error("Key is not a valid UTF-8: {0}")]
156    Utf8(#[from] std::string::FromUtf8Error),
157
158    /// Magic value is incorrect.
159    ///
160    /// This file is either corrupted or not a NetHSM backup.
161    #[error("Bad magic value: {0:X?}")]
162    BadMagic(Vec<u8>),
163
164    /// Version number is not recognized.
165    ///
166    /// This library supports only version `0` backups.
167    #[error(
168        "Unsupported backup version number: {backup_version:?}. The highest supported version is {highest_supported_version}"
169    )]
170    BadVersion {
171        /// The highest version that is supported by the current implementation..
172        highest_supported_version: u8,
173
174        /// The version of the backup file.
175        backup_version: Vec<u8>,
176    },
177}
178
179/// Custom [`Result`] wrapper for [`Error`]s that may occur when using `nethsm_backup`.
180pub type Result<T> = std::result::Result<T, Error>;
181
182/// Magic value that is contained in all NetHSM backups.
183const MAGIC: &[u8] = b"_NETHSM_BACKUP_";
184
185/// Read 3 bytes from the provided reader and interprets it as a [usize].
186fn read_usize(reader: &mut impl Read) -> std::io::Result<usize> {
187    const LEN: usize = size_of::<usize>();
188    let mut bytes = [0; LEN];
189    // read exactly 3 bytes
190    reader.read_exact(&mut bytes[LEN - 3..])?;
191    Ok(usize::from_be_bytes(bytes))
192}
193
194/// Read a byte vector from the underlying reader.
195///
196/// A byte vector is always stored as a [usize] (see [read_usize]) and
197/// then a number of bytes.
198fn read_field(reader: &mut impl Read) -> Result<Vec<u8>> {
199    let len = read_usize(reader)?;
200    let mut field = vec![0; len];
201    reader.read_exact(&mut field)?;
202    Ok(field)
203}
204
205/// Check if the reader contains correct [MAGIC] value.
206///
207/// # Errors
208///
209/// Returns:
210/// * [Error::BadMagic] if an unrecognized magic value is found.
211/// * [Error::Io] if an I/O error occurs.
212fn check_magic(reader: &mut impl Read) -> Result<()> {
213    let mut magic = [0; MAGIC.len()];
214    reader.read_exact(&mut magic)?;
215    if MAGIC != magic {
216        return Err(Error::BadMagic(magic.into()));
217    }
218    Ok(())
219}
220
221/// Check if the reader contains version number that is understood.
222///
223/// # Errors
224///
225/// Returns:
226/// * [Error::BadVersion] if an unrecognized version value is found.
227/// * [Error::Io] if an I/O error occurs.
228fn check_version(reader: &mut impl Read) -> Result<()> {
229    let mut version = [0; 1];
230    reader.read_exact(&mut version)?;
231    let version = version[0];
232    if version != 0 {
233        return Err(Error::BadVersion {
234            highest_supported_version: 0,
235            backup_version: vec![version],
236        });
237    }
238    Ok(())
239}
240
241/// Data of a NetHSM backup.
242///
243/// This object contains the data of a successfully parsed and well-formed NetHSM backup.
244#[derive(Debug)]
245pub struct Backup {
246    salt: Vec<u8>,
247    encrypted_version: Vec<u8>,
248    encrypted_domain_key: Vec<u8>,
249    items: Vec<Vec<u8>>,
250}
251
252impl Backup {
253    /// Parse the backup from a reader.
254    ///
255    /// The reader must contain a well-formed, valid NetHSM backup file.
256    ///
257    /// # Errors
258    ///
259    /// Returns:
260    /// * [Error::BadVersion] if an unrecognized version value is found.
261    /// * [Error::BadMagic] if an unrecognized version value is found.
262    /// * [Error::Io] if an I/O error occurs when reading the backup.
263    pub fn parse(mut reader: impl Read) -> Result<Self> {
264        check_magic(&mut reader)?;
265        check_version(&mut reader)?;
266
267        let salt = read_field(&mut reader)?;
268        let encrypted_version = read_field(&mut reader)?;
269        let encrypted_domain_key = read_field(&mut reader)?;
270
271        let mut items = vec![];
272        loop {
273            match read_usize(&mut reader) {
274                Ok(len) => {
275                    let mut field = vec![0; len];
276                    reader.read_exact(&mut field)?;
277                    items.push(field);
278                }
279                Err(error) if error.kind() == ErrorKind::UnexpectedEof => {
280                    break;
281                }
282                Err(error) => {
283                    return Err(error)?;
284                }
285            }
286        }
287
288        Ok(Self {
289            salt,
290            encrypted_version,
291            encrypted_domain_key,
292            items,
293        })
294    }
295
296    /// Create a [`BackupDecryptor`] that will decrypt items with the provided passphrase.
297    ///
298    /// # Errors
299    ///
300    /// Even though this function returns a `Result` it is unlikely to fail since all parameters are
301    /// static.
302    pub fn decrypt(&self, passphrase: &[u8]) -> Result<BackupDecryptor<'_>> {
303        BackupDecryptor::new(self, passphrase)
304    }
305}
306
307/// Backup decryptor which decrypts backup items on the fly.
308pub struct BackupDecryptor<'a> {
309    backup: &'a Backup,
310    cipher: Aes256Gcm,
311}
312
313impl std::fmt::Debug for BackupDecryptor<'_> {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        f.debug_struct("BackupDecryptor")
316            .field("backup", self.backup)
317            .field("cipher", &"[REDACTED]")
318            .finish()
319    }
320}
321
322impl<'a> BackupDecryptor<'a> {
323    /// Create a new [`BackupDecryptor`] using a [`Backup`] and a passphrase.
324    ///
325    /// # Errors
326    ///
327    /// Even though this function returns a `Result` it is unlikely to fail since all parameters are
328    /// static.
329    fn new(backup: &'a Backup, passphrase: &[u8]) -> Result<Self> {
330        let mut key = [0; 32];
331        scrypt(
332            passphrase,
333            &backup.salt,
334            &Params::new(14, 8, 16, 32).map_err(|e| {
335                error!("Scrypt parameters are invalid. This should not happen as they are statically chosen: {e:?}");
336                Error::InvalidScryptParams
337            })?,
338            &mut key,
339        )
340        .map_err(|e|  {
341            error!("Scrypt derivation failed: {e:?}");
342            Error::ScryptKeyDerivation
343        })?;
344        let cipher = Aes256Gcm::new(&key.into());
345        Ok(Self { backup, cipher })
346    }
347
348    /// Decrypts `ciphertext` while verifying additional data (`aad`).
349    ///
350    /// # Errors
351    ///
352    /// Returns:
353    /// * [Error::Decryption] if a decryption error is encountered, for example the ciphertext is of
354    ///   incorrect length, has been tampered with, the decryption passphrase is wrong or the
355    ///   additional authenticated data is incorrect.
356    fn decrypt(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
357        let Some((nonce, msg)) = ciphertext.split_at_checked(12) else {
358            return Err(Error::Decryption);
359        };
360
361        let payload = aes_gcm::aead::Payload { msg, aad };
362
363        let plaintext = self.cipher.decrypt(nonce.into(), payload).map_err(|e| {
364            error!("Decryption failed: {e:?}");
365            Error::Decryption
366        })?;
367        Ok(plaintext)
368    }
369
370    /// Decrypted backup version.
371    ///
372    /// # Errors
373    ///
374    /// Returns:
375    /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted
376    ///   version is of incorrect length, has been tampered with, the decryption passphrase is wrong
377    ///   or the additional authenticated data is incorrect (e.g. a different encrypted piece of
378    ///   data is impersonating the backup version).
379    pub fn version(&self) -> Result<Vec<u8>> {
380        self.decrypt(&self.backup.encrypted_version, b"backup-version")
381    }
382
383    /// Decrypted domain key.
384    ///
385    /// # Errors
386    ///
387    /// Returns:
388    /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted domain
389    ///   key is of incorrect length, has been tampered with, the decryption passphrase is wrong or
390    ///   the additional authenticated data is incorrect (e.g. a different encrypted piece of data
391    ///   is impersonating the domain key).
392    pub fn domain_key(&self) -> Result<Vec<u8>> {
393        self.decrypt(&self.backup.encrypted_domain_key, b"domain-key")
394    }
395
396    /// Returns an iterator over backup entries.
397    ///
398    /// The entries are pairs of keys (which are strings) and values (byte vectors).
399    /// Since the entries are decrypted as they are being read the pairs are wrapped in
400    /// [`Result`]s.
401    ///
402    /// # Errors
403    ///
404    /// This function does not fail but reading the inner iterator may return errors:
405    /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted entry
406    ///   is of incorrect length, has been tampered with, the decryption passphrase is wrong or the
407    ///   additional authenticated data is incorrect (e.g. a different encrypted piece of data is
408    ///   impersonating the backup entry).
409    /// * [Error::Utf8] if the entry's key is not a well-formed UTF-8 string.
410    pub fn items_iter(&'a self) -> impl Iterator<Item = Result<(String, Vec<u8>)>> + 'a {
411        BackupItemDecryptor {
412            decryptor: self,
413            inner: self.backup.items.iter(),
414        }
415    }
416}
417
418/// Iterates over the entries of a backup and decrypts them on the fly.
419///
420/// This struct is a wrapper over an iterator of items of a [`Backup`].
421/// It keeps a state of the current item being processed.
422/// Each item is decrypted and then split into a UTF-8 string key and a value that is a byte vector.
423struct BackupItemDecryptor<'a> {
424    decryptor: &'a BackupDecryptor<'a>,
425    inner: Iter<'a, Vec<u8>>,
426}
427
428impl Iterator for BackupItemDecryptor<'_> {
429    type Item = Result<(String, Vec<u8>)>;
430
431    /// Return next pair of key and value.
432    ///
433    /// # Errors
434    ///
435    /// Returns
436    /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted entry
437    ///   is of incorrect length, has been tampered with, the decryption passphrase is wrong or the
438    ///   additional authenticated data is incorrect (e.g. a different encrypted piece of data is
439    ///   impersonating the backup entry).
440    /// * [Error::Utf8] if the entry's key is not a well-formed UTF-8 string.
441    fn next(&mut self) -> Option<Self::Item> {
442        self.inner.next().map(|item| {
443            let decrypted = self.decryptor.decrypt(item, b"backup")?;
444            let mut reader = std::io::Cursor::new(decrypted);
445            let key = String::from_utf8(read_field(&mut reader)?)?;
446            let mut value = vec![];
447            reader.read_to_end(&mut value)?;
448            Ok((key, value))
449        })
450    }
451}
452
453/// Validates a [backup].
454///
455/// Parses a previously created backup file. If `passphrase` is
456/// [`Some`], additionally decrypts the backup and verifies the
457/// encrypted backup version number.
458///
459/// # Errors
460///
461/// Returns an error if validating a [backup] fails:
462///
463/// - the magic number is missing in the file
464/// - the version number is unknown
465/// - the provided passphrase is incorrect
466///
467/// # Examples
468///
469/// ```no_run
470/// use nethsm::{Connection, ConnectionSecurity, Credentials, NetHsm};
471/// use nethsm_backup::validate_backup;
472/// use signstar_crypto::passphrase::Passphrase;
473///
474/// # fn main() -> testresult::TestResult {
475/// // create a connection with a user in the Backup role
476/// let nethsm = NetHsm::new(
477///     Connection::new(
478///         "https://example.org/api/v1".try_into()?,
479///         ConnectionSecurity::Unsafe,
480///     ),
481///     Some(Credentials::new(
482///         "backup1".parse()?,
483///         Some(Passphrase::new("passphrase".to_string())),
484///     )),
485///     None,
486///     None,
487/// )?;
488///
489/// // create a backup and write it to file
490/// std::fs::write("nethsm.bkp", nethsm.backup()?)?;
491///
492/// // check for consistency only
493/// validate_backup(&mut std::fs::File::open("nethsm.bkp")?, None)?;
494///
495/// // check for correct passphrase by decrypting and validating the encrypted backup version
496/// validate_backup(
497///     &mut std::fs::File::open("nethsm.bkp")?,
498///     Passphrase::new("a sample password".into()),
499/// )?;
500/// # Ok(())
501/// # }
502/// ```
503/// [backup]: https://docs.nitrokey.com/nethsm/administration#backup
504pub fn validate_backup(
505    reader: &mut impl Read,
506    passphrase: impl Into<Option<Passphrase>>,
507) -> Result<()> {
508    let passphrase = passphrase.into();
509    let backup = Backup::parse(reader)?;
510    if let Some(passphrase) = passphrase {
511        let decryptor = backup.decrypt(passphrase.expose_borrowed().as_bytes())?;
512        let version = decryptor.version()?;
513        if version.len() != 1 || version[0] != 0 {
514            return Err(Error::BadVersion {
515                highest_supported_version: 0,
516                backup_version: version,
517            });
518        }
519    }
520    Ok(())
521}