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 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).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_first_chunk::<12>() else {
358 return Err(Error::Decryption);
359 };
360
361 let payload = aes_gcm::aead::Payload { msg, aad };
362
363 let plaintext = self
364 .cipher
365 .decrypt(&(*nonce).into(), payload)
366 .map_err(|e| {
367 error!("Decryption failed: {e:?}");
368 Error::Decryption
369 })?;
370 Ok(plaintext)
371 }
372
373 /// Decrypted backup version.
374 ///
375 /// # Errors
376 ///
377 /// Returns:
378 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted
379 /// version is of incorrect length, has been tampered with, the decryption passphrase is wrong
380 /// or the additional authenticated data is incorrect (e.g. a different encrypted piece of
381 /// data is impersonating the backup version).
382 pub fn version(&self) -> Result<Vec<u8>> {
383 self.decrypt(&self.backup.encrypted_version, b"backup-version")
384 }
385
386 /// Decrypted domain key.
387 ///
388 /// # Errors
389 ///
390 /// Returns:
391 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted domain
392 /// key is of incorrect length, has been tampered with, the decryption passphrase is wrong or
393 /// the additional authenticated data is incorrect (e.g. a different encrypted piece of data
394 /// is impersonating the domain key).
395 pub fn domain_key(&self) -> Result<Vec<u8>> {
396 self.decrypt(&self.backup.encrypted_domain_key, b"domain-key")
397 }
398
399 /// Returns an iterator over backup entries.
400 ///
401 /// The entries are pairs of keys (which are strings) and values (byte vectors).
402 /// Since the entries are decrypted as they are being read the pairs are wrapped in
403 /// [`Result`]s.
404 ///
405 /// # Errors
406 ///
407 /// This function does not fail but reading the inner iterator may return errors:
408 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted entry
409 /// is of incorrect length, has been tampered with, the decryption passphrase is wrong or the
410 /// additional authenticated data is incorrect (e.g. a different encrypted piece of data is
411 /// impersonating the backup entry).
412 /// * [Error::Utf8] if the entry's key is not a well-formed UTF-8 string.
413 pub fn items_iter(&'a self) -> impl Iterator<Item = Result<(String, Vec<u8>)>> + 'a {
414 BackupItemDecryptor {
415 decryptor: self,
416 inner: self.backup.items.iter(),
417 }
418 }
419}
420
421/// Iterates over the entries of a backup and decrypts them on the fly.
422///
423/// This struct is a wrapper over an iterator of items of a [`Backup`].
424/// It keeps a state of the current item being processed.
425/// Each item is decrypted and then split into a UTF-8 string key and a value that is a byte vector.
426struct BackupItemDecryptor<'a> {
427 decryptor: &'a BackupDecryptor<'a>,
428 inner: Iter<'a, Vec<u8>>,
429}
430
431impl Iterator for BackupItemDecryptor<'_> {
432 type Item = Result<(String, Vec<u8>)>;
433
434 /// Return next pair of key and value.
435 ///
436 /// # Errors
437 ///
438 /// Returns
439 /// * [Error::Decryption] if a decryption error is encountered, for example the encrypted entry
440 /// is of incorrect length, has been tampered with, the decryption passphrase is wrong or the
441 /// additional authenticated data is incorrect (e.g. a different encrypted piece of data is
442 /// impersonating the backup entry).
443 /// * [Error::Utf8] if the entry's key is not a well-formed UTF-8 string.
444 fn next(&mut self) -> Option<Self::Item> {
445 self.inner.next().map(|item| {
446 let decrypted = self.decryptor.decrypt(item, b"backup")?;
447 let mut reader = std::io::Cursor::new(decrypted);
448 let key = String::from_utf8(read_field(&mut reader)?)?;
449 let mut value = vec![];
450 reader.read_to_end(&mut value)?;
451 Ok((key, value))
452 })
453 }
454}
455
456/// Validates a [backup].
457///
458/// Parses a previously created backup file. If `passphrase` is
459/// [`Some`], additionally decrypts the backup and verifies the
460/// encrypted backup version number.
461///
462/// # Errors
463///
464/// Returns an error if validating a [backup] fails:
465///
466/// - the magic number is missing in the file
467/// - the version number is unknown
468/// - the provided passphrase is incorrect
469///
470/// # Examples
471///
472/// ```no_run
473/// use nethsm::{Connection, ConnectionSecurity, Credentials, NetHsm};
474/// use nethsm_backup::validate_backup;
475/// use signstar_crypto::passphrase::Passphrase;
476///
477/// # fn main() -> testresult::TestResult {
478/// // create a connection with a user in the Backup role
479/// let nethsm = NetHsm::new(
480/// Connection::new(
481/// "https://example.org/api/v1".try_into()?,
482/// ConnectionSecurity::Unsafe,
483/// ),
484/// Some(Credentials::new(
485/// "backup1".parse()?,
486/// Some(Passphrase::new("passphrase".to_string())),
487/// )),
488/// None,
489/// None,
490/// )?;
491///
492/// // create a backup and write it to file
493/// std::fs::write("nethsm.bkp", nethsm.backup()?)?;
494///
495/// // check for consistency only
496/// validate_backup(&mut std::fs::File::open("nethsm.bkp")?, None)?;
497///
498/// // check for correct passphrase by decrypting and validating the encrypted backup version
499/// validate_backup(
500/// &mut std::fs::File::open("nethsm.bkp")?,
501/// Passphrase::new("a sample password".into()),
502/// )?;
503/// # Ok(())
504/// # }
505/// ```
506/// [backup]: https://docs.nitrokey.com/nethsm/administration#backup
507pub fn validate_backup(
508 reader: &mut impl Read,
509 passphrase: impl Into<Option<Passphrase>>,
510) -> Result<()> {
511 let passphrase = passphrase.into();
512 let backup = Backup::parse(reader)?;
513 if let Some(passphrase) = passphrase {
514 let decryptor = backup.decrypt(passphrase.expose_borrowed().as_bytes())?;
515 let version = decryptor.version()?;
516 if version.len() != 1 || version[0] != 0 {
517 return Err(Error::BadVersion {
518 highest_supported_version: 0,
519 backup_version: version,
520 });
521 }
522 }
523 Ok(())
524}