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}