goldboot_image/
lib.rs

1//!
2
3use crate::qcow::Qcow3;
4use aes_gcm::KeyInit;
5use aes_gcm::{aead::Aead, Aes256Gcm, Key, Nonce};
6use anyhow::bail;
7use anyhow::Result;
8use binrw::{BinRead, BinReaderExt, BinWrite};
9use rand::Rng;
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use std::ffi::CStr;
14use std::path::PathBuf;
15use std::{
16    fs::File,
17    io::{BufReader, Cursor, Read, Seek, SeekFrom, Write},
18    path::Path,
19    time::{SystemTime, UNIX_EPOCH},
20};
21use strum::{Display, EnumIter};
22use tracing::{debug, info, trace};
23
24pub mod qcow;
25
26/// Supported system architectures for goldboot images.
27#[derive(
28    BinRead, BinWrite, Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, EnumIter, Display,
29)]
30#[serde(tag = "arch")]
31#[brw(repr(u8))]
32pub enum ImageArch {
33    Amd64,
34    Arm64,
35    I386,
36    Mips,
37    Mips64,
38    S390x,
39}
40
41impl ImageArch {
42    pub fn as_github_string(&self) -> String {
43        match self {
44            ImageArch::Amd64 => "x86_64",
45            ImageArch::Arm64 => todo!(),
46            ImageArch::I386 => todo!(),
47            ImageArch::Mips => todo!(),
48            ImageArch::Mips64 => todo!(),
49            ImageArch::S390x => todo!(),
50        }
51        .to_string()
52    }
53}
54
55impl Default for ImageArch {
56    fn default() -> Self {
57        match std::env::consts::ARCH {
58            "x86" => ImageArch::I386,
59            "x86_64" => ImageArch::Amd64,
60            "aarch64" => ImageArch::Arm64,
61            "mips" => ImageArch::Mips,
62            "mips64" => ImageArch::Mips64,
63            "s390x" => ImageArch::S390x,
64            _ => panic!("Unknown CPU architecture: {}", std::env::consts::ARCH),
65        }
66    }
67}
68
69impl TryFrom<String> for ImageArch {
70    type Error = anyhow::Error;
71    fn try_from(s: String) -> Result<Self> {
72        match s.to_lowercase().as_str() {
73            "amd64" => Ok(ImageArch::Amd64),
74            "x86_64" => Ok(ImageArch::Amd64),
75            "arm64" => Ok(ImageArch::Arm64),
76            "aarch64" => Ok(ImageArch::Arm64),
77            "i386" => Ok(ImageArch::I386),
78            _ => bail!("Unknown architecture: {s}"),
79        }
80    }
81}
82
83/// Represents a goldboot image on disk.
84///
85/// # Binary format
86///
87/// | Section             | Encryption Key    |
88/// |---------------------|-------------------|
89/// | Primary Header      | None              |
90/// | Protected Header    | Password + SHA256 |
91/// | Image Config        | Password + SHA256 |
92/// | Cluster Table       | Cluster Key       |
93/// | Digest Table        | Password + SHA256 |
94/// | Directory           | Password + SHA256 |
95///
96/// The target data is divided into equal size sections called "blocks". Blocks
97/// that are nonzero will have an associated "cluster" allocated in the image
98/// file. Clusters are variable in size and ideally smaller than their
99/// associated blocks (due to compression). If a block does not have an
100/// associated cluster, that block is zero.
101#[derive(Debug)]
102pub struct ImageHandle {
103    /// The primary file header
104    pub primary_header: PrimaryHeader,
105
106    /// The secondary header
107    pub protected_header: Option<ProtectedHeader>,
108
109    /// The encoded config used to build the image
110    pub config: Option<Vec<u8>>,
111
112    /// The digest table
113    pub digest_table: Option<DigestTable>,
114
115    /// The section directory
116    pub directory: Option<Directory>,
117
118    /// The filesystem path to the image file
119    pub path: std::path::PathBuf,
120
121    /// The size in bytes of the image file on disk
122    pub file_size: u64,
123
124    /// The image's ID (SHA256 hash)
125    pub id: String,
126}
127
128/// The cluster compression algorithm.
129#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
130#[brw(repr(u8))]
131pub enum ClusterCompressionType {
132    /// Clusters will not be compressed
133    None = 0,
134
135    /// Clusters will be compressed with Zstandard
136    Zstd = 1,
137}
138
139/// The cluster encryption algorithm.
140#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
141#[brw(repr(u8))]
142pub enum ClusterEncryptionType {
143    /// Clusters will not be encrypted
144    None = 0,
145
146    /// Clusters will be encrypted with AES256 GCM after compression
147    Aes256 = 1,
148}
149
150/// The header encryption algorithm.
151#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
152#[brw(repr(u8))]
153pub enum HeaderEncryptionType {
154    /// The header will not be encrypted
155    None = 0,
156
157    /// The header will be encrypted with AES256 GCM
158    Aes256 = 1,
159}
160
161/// Contains metadata which is always plaintext. Anything potentially useful to
162/// an attacker should instead reside in the protected header unless the user
163/// may want to read it without decrypting the image first.
164#[derive(BinRead, BinWrite, Debug, Eq, PartialEq)]
165#[brw(magic = b"\xc0\x1d\xb0\x01", big)]
166pub struct PrimaryHeader {
167    /// The format version
168    #[br(assert(version == 1))]
169    pub version: u8,
170
171    /// The total size of all blocks combined in bytes
172    pub size: u64,
173
174    /// Image creation time
175    pub timestamp: u64,
176
177    /// The encryption type for metadata
178    pub encryption_type: HeaderEncryptionType,
179
180    /// A copy of the name field from the config
181    pub name: [u8; 64],
182
183    /// System architecture
184    pub arch: ImageArch,
185
186    /// Directory nonce
187    pub directory_nonce: [u8; 12],
188
189    /// The byte offset of the directory
190    pub directory_offset: u64,
191
192    /// The size of the directory in bytes
193    pub directory_size: u32,
194
195    /// Whether the image is public
196    pub public: u8,
197
198    /// Extra space for the future
199    pub reserved: [u8; 64],
200}
201
202impl PrimaryHeader {
203    pub fn name(&self) -> String {
204        unsafe { CStr::from_ptr(self.name.as_ptr() as *const std::ffi::c_char) }
205            .to_string_lossy()
206            .into_owned()
207    }
208
209    pub fn is_public(&self) -> bool {
210        return self.public == 1u8;
211    }
212}
213
214/// Contains metadata which may be encrypted.
215#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
216#[brw(big)]
217pub struct ProtectedHeader {
218    /// The size in bytes of each disk block
219    pub block_size: u32,
220
221    /// The number of populated clusters in this image
222    pub cluster_count: u32,
223
224    /// The compression algorithm used on clusters
225    pub cluster_compression: ClusterCompressionType,
226
227    /// The encryption type for the digest table and all clusters
228    pub cluster_encryption: ClusterEncryptionType,
229
230    /// The number of cluster nonces if encryption is enabled
231    pub nonce_count: u32,
232
233    /// A nonce for each cluster if encryption is enabled
234    #[br(count = nonce_count)]
235    pub nonce_table: Vec<[u8; 12]>,
236
237    /// The key for all clusters if encryption is enabled
238    pub cluster_key: [u8; 32],
239}
240
241#[derive(BinRead, BinWrite, Debug)]
242#[brw(big)]
243pub struct Directory {
244    /// Protected header nonce
245    pub protected_nonce: [u8; 12],
246
247    /// The size of the protected header in bytes
248    pub protected_size: u32,
249
250    /// The nonce value used to encrypt the config
251    pub config_nonce: [u8; 12],
252
253    /// The byte offset of the config
254    pub config_offset: u64,
255
256    /// The size of the config in bytes
257    pub config_size: u32,
258
259    /// The nonce value used to encrypt the digest table
260    pub digest_table_nonce: [u8; 12],
261
262    /// The byte offset of the digest table
263    pub digest_table_offset: u64,
264
265    /// The size of the digest table in bytes
266    pub digest_table_size: u32,
267}
268
269#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
270#[brw(big)]
271pub struct DigestTable {
272    /// The number of digests (and therefore the number of clusters)
273    pub digest_count: u32,
274
275    /// A digest for each cluster
276    #[br(count = digest_count)]
277    pub digest_table: Vec<DigestTableEntry>,
278}
279
280/// An entry in the digest table which corresponds to one cluster.
281#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
282#[brw(big)]
283pub struct DigestTableEntry {
284    /// The cluster's offset in the image file
285    pub cluster_offset: u64,
286
287    /// The block's offset in the real data
288    pub block_offset: u64,
289
290    /// The SHA256 hash of the original block before compression and encryption
291    pub digest: [u8; 32],
292}
293
294/// Represents a data cluster in the image file. Each cluster corresponds to a
295/// fixed-size block in the user data.
296#[derive(BinRead, BinWrite, Debug)]
297#[brw(big)]
298pub struct Cluster {
299    /// The size of the cluster in bytes
300    pub size: u32,
301
302    /// The cluster data which might be compressed and encrypted
303    #[br(count = size)]
304    pub data: Vec<u8>,
305}
306
307/// Build an encryption key from the given password.
308fn new_key(password: String) -> Aes256Gcm {
309    // Hash so it's the correct length
310    Aes256Gcm::new(&Sha256::new().chain_update(password.as_bytes()).finalize())
311}
312
313/// Hash the entire image file to produce the image ID.
314pub fn compute_id(path: impl AsRef<Path>) -> Result<String> {
315    let mut file = File::open(&path)?;
316    let mut hasher = Sha256::new();
317
318    std::io::copy(&mut file, &mut hasher)?;
319    Ok(hex::encode(hasher.finalize()))
320}
321
322impl ImageHandle {
323    /// Load all sections into memory except the cluster table. If the image is
324    /// encrypted, the sections will be decrypted.
325    pub fn load(&mut self, password: Option<String>) -> Result<()> {
326        let mut file = File::open(&self.path)?;
327
328        let cipher = new_key(password.unwrap_or("".to_string()));
329
330        // Load the directory first because other sections rely on it
331        file.seek(SeekFrom::Start(self.primary_header.directory_offset))?;
332        let directory: Directory = match self.primary_header.encryption_type {
333            HeaderEncryptionType::None => file.read_be()?,
334            HeaderEncryptionType::Aes256 => {
335                let mut directory_bytes = vec![0u8; self.primary_header.directory_size as usize];
336                file.read_exact(&mut directory_bytes)?;
337
338                let directory_bytes = cipher.decrypt(
339                    Nonce::from_slice(&self.primary_header.directory_nonce),
340                    directory_bytes.as_ref(),
341                )?;
342                Cursor::new(directory_bytes).read_be()?
343            }
344        };
345
346        // Load the protected header
347        file.seek(SeekFrom::Start(0))?;
348
349        // Throw this away so we're at the correct offset
350        let _primary: PrimaryHeader = file.read_be()?;
351        let protected_header: ProtectedHeader = match self.primary_header.encryption_type {
352            HeaderEncryptionType::None => file.read_be()?,
353            HeaderEncryptionType::Aes256 => {
354                let mut protected_header_bytes = vec![0u8; directory.protected_size as usize];
355                file.read_exact(&mut protected_header_bytes)?;
356
357                let protected_header_bytes = cipher.decrypt(
358                    Nonce::from_slice(&directory.protected_nonce),
359                    protected_header_bytes.as_ref(),
360                )?;
361                Cursor::new(protected_header_bytes).read_be()?
362            }
363        };
364
365        // Load config
366        file.seek(SeekFrom::Start(directory.config_offset))?;
367        let mut config_bytes = vec![0u8; directory.config_size as usize];
368        file.read_exact(&mut config_bytes)?;
369
370        self.config = match self.primary_header.encryption_type {
371            HeaderEncryptionType::None => Some(config_bytes),
372            HeaderEncryptionType::Aes256 => Some(cipher.decrypt(
373                Nonce::from_slice(&directory.config_nonce),
374                config_bytes.as_ref(),
375            )?),
376        };
377
378        // Load the digest table
379        file.seek(SeekFrom::Start(directory.digest_table_offset))?;
380        let digest_table: DigestTable = match self.primary_header.encryption_type {
381            HeaderEncryptionType::None => file.read_be()?,
382            HeaderEncryptionType::Aes256 => {
383                let mut digest_table_bytes = vec![0u8; directory.digest_table_size as usize];
384                file.read_exact(&mut digest_table_bytes)?;
385
386                let digest_table_bytes = cipher.decrypt(
387                    Nonce::from_slice(&directory.digest_table_nonce),
388                    digest_table_bytes.as_ref(),
389                )?;
390                Cursor::new(digest_table_bytes).read_be()?
391            }
392        };
393
394        // Modify the current image handle finally
395        self.directory = Some(directory);
396        self.protected_header = Some(protected_header);
397        self.digest_table = Some(digest_table);
398        Ok(())
399    }
400
401    /// Open a new handle on the given file.
402    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
403        let path = path.as_ref();
404        let mut file = File::open(path)?;
405
406        debug!(path = ?path, "Opening image");
407
408        // Read primary header (always plaintext)
409        let primary_header: PrimaryHeader = file.read_be()?;
410        debug!(primary_header = ?primary_header, "Primary header");
411
412        // Get image ID
413        let id = if let Some(stem) = path.file_stem() {
414            if Regex::new("[A-Fa-f0-9]{64}")?.is_match(stem.to_str().unwrap()) {
415                stem.to_str().unwrap().to_string()
416            } else {
417                compute_id(&path).unwrap()
418            }
419        } else {
420            compute_id(&path).unwrap()
421        };
422
423        if primary_header.encryption_type == HeaderEncryptionType::None {
424            // Read protected header
425            let protected_header: ProtectedHeader = file.read_be()?;
426
427            // Read directory
428            file.seek(SeekFrom::Start(primary_header.directory_offset))?;
429            let directory: Directory = file.read_be()?;
430
431            // Read config
432            let mut config = vec![0u8; directory.config_size as usize];
433            file.seek(SeekFrom::Start(directory.config_offset))?;
434            file.read_exact(&mut config)?;
435
436            Ok(Self {
437                id,
438                primary_header,
439                protected_header: Some(protected_header),
440                config: Some(config),
441                digest_table: None,
442                directory: Some(directory),
443                path: path.to_path_buf(),
444                file_size: std::fs::metadata(&path)?.len(),
445            })
446        } else {
447            Ok(Self {
448                id,
449                primary_header,
450                protected_header: None,
451                config: None,
452                digest_table: None,
453                directory: None,
454                path: path.to_path_buf(),
455                file_size: std::fs::metadata(&path)?.len(),
456            })
457        }
458    }
459
460    /// Modify the password and re-encrypt all encrypted sections. This doesn't
461    /// re-encrypt the clusters because they are encrypted with the cluster key.
462    pub fn change_password(&self, _old_password: String, new_password: String) -> Result<()> {
463        // Create the cipher and a RNG for the nonces
464        let _cipher = new_key(new_password);
465        let _rng = rand::thread_rng();
466
467        todo!()
468    }
469
470    /// TODO multi threaded WriteWorkers
471    /// TODO write backup GPT header
472
473    /// Write the image contents out to disk.
474    pub fn write<F: Fn(u64, u64) -> ()>(&self, dest: impl AsRef<Path>, progress: F) -> Result<()> {
475        if self.protected_header.is_none() || self.digest_table.is_none() {
476            bail!("Image not loaded");
477        }
478
479        let protected_header = self.protected_header.clone().unwrap();
480        let digest_table = self.digest_table.clone().unwrap().digest_table;
481
482        let cluster_cipher =
483            Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&protected_header.cluster_key));
484
485        let dest = dest.as_ref();
486        info!(image = ?self, dest = ?dest, "Writing goldboot image");
487
488        let mut dest = std::fs::OpenOptions::new()
489            .create(true)
490            .write(true)
491            .read(true)
492            .open(dest)?;
493
494        // Open the cluster table for reading
495        let mut cluster_table = BufReader::new(File::open(&self.path)?);
496
497        // Extend regular files if necessary
498        // TODO also check size of block devices
499        let dest_metadata = dest.metadata()?;
500        if dest_metadata.is_file() && dest_metadata.len() < self.primary_header.size {
501            dest.set_len(self.primary_header.size)?;
502        }
503
504        let mut block = vec![0u8; protected_header.block_size as usize];
505
506        // Write all of the clusters that have changed
507        for i in 0..protected_header.cluster_count as usize {
508            // Load digest table entry
509            let entry = &digest_table[i];
510
511            // Jump to the block corresponding to the cluster
512            dest.seek(SeekFrom::Start(entry.block_offset))?;
513
514            // Hash the block to avoid unnecessary writes
515            let hash: [u8; 32] = match dest.read_exact(&mut block) {
516                Ok(_) => Sha256::new().chain_update(&block).finalize().into(),
517                Err(_) => {
518                    // TODO check for EOF error
519                    [0u8; 32]
520                }
521            };
522
523            if hash != entry.digest {
524                // Read cluster
525                cluster_table.seek(SeekFrom::Start(entry.cluster_offset))?;
526                let mut cluster: Cluster = cluster_table.read_be()?;
527
528                trace!(
529                    cluster_size = cluster.size,
530                    cluster_offset = entry.cluster_offset,
531                    "Read dirty cluster",
532                );
533
534                // Reverse encryption
535                cluster.data = match protected_header.cluster_encryption {
536                    ClusterEncryptionType::None => cluster.data,
537                    ClusterEncryptionType::Aes256 => cluster_cipher.decrypt(
538                        Nonce::from_slice(&protected_header.nonce_table[i]),
539                        cluster.data.as_ref(),
540                    )?,
541                };
542
543                // Reverse compression
544                cluster.data = match protected_header.cluster_compression {
545                    ClusterCompressionType::None => cluster.data,
546                    ClusterCompressionType::Zstd => {
547                        zstd::decode_all(std::io::Cursor::new(&cluster.data))?
548                    }
549                };
550
551                trace!(
552                    block_offset = entry.block_offset,
553                    block_size = cluster.data.len(),
554                    "Writing block",
555                );
556
557                // Write the cluster to the block
558                dest.seek(SeekFrom::Start(entry.block_offset))?;
559                dest.write_all(&cluster.data)?;
560            }
561
562            progress(
563                protected_header.block_size as u64,
564                protected_header.cluster_count as u64 * protected_header.block_size as u64,
565            );
566        }
567
568        Ok(())
569    }
570}
571
572pub struct ImageBuilder {
573    name: String,
574    config: Vec<u8>,
575    password: Option<String>,
576    public: bool,
577    dest: PathBuf,
578    progress: Box<dyn Fn(u64, u64) -> ()>,
579}
580
581impl ImageBuilder {
582    pub fn new(dest: impl AsRef<Path>) -> Self {
583        Self {
584            name: "".into(),
585            config: Vec::new(),
586            password: None,
587            public: false,
588            dest: dest.as_ref().to_path_buf(),
589            progress: Box::new(|_, _| {}),
590        }
591    }
592
593    pub fn name(mut self, name: &str) -> Self {
594        self.name = name.to_string();
595        self
596    }
597
598    pub fn config(mut self, config: Vec<u8>) -> Self {
599        self.config = config;
600        self
601    }
602
603    pub fn public(mut self, public: bool) -> Self {
604        self.public = public;
605        self
606    }
607
608    pub fn password(mut self, password: &str) -> Self {
609        self.password = Some(password.to_string());
610        self
611    }
612
613    pub fn password_opt(mut self, password: Option<String>) -> Self {
614        self.password = password;
615        self
616    }
617
618    pub fn progress<'a, F>(&'a mut self, progress: F) -> &'a mut Self
619    where
620        F: Fn(u64, u64) + 'static,
621    {
622        self.progress = Box::new(progress);
623        self
624    }
625
626    /// Convert a qcow image into a goldboot image.
627    pub fn convert(self, source: &Qcow3, size: u64) -> Result<ImageHandle> {
628        info!(name = self.name, "Exporting storage to goldboot image");
629
630        assert!(
631            source.header.size >= size,
632            "source.header.size = {}, size = {}",
633            source.header.size,
634            size
635        );
636
637        let mut dest_file = File::create(&self.dest)?;
638        let mut source_file = File::open(&source.path)?;
639
640        // Prepare cipher and RNG if the image header should be encrypted
641        let header_cipher = new_key(self.password.clone().unwrap_or("".to_string()));
642        let mut rng = rand::thread_rng();
643
644        // Prepare directory
645        let mut directory = Directory {
646            protected_nonce: rng.gen::<[u8; 12]>(),
647            protected_size: 0,
648            config_nonce: rng.gen::<[u8; 12]>(),
649            config_offset: 0,
650            config_size: 0,
651            digest_table_nonce: rng.gen::<[u8; 12]>(),
652            digest_table_offset: 0,
653            digest_table_size: 0,
654        };
655
656        // Prepare primary header
657        let mut primary_header = PrimaryHeader {
658            version: 1,
659            arch: ImageArch::Amd64, // TODO
660            size,
661            directory_nonce: rng.gen::<[u8; 12]>(),
662            directory_offset: 0,
663            directory_size: 0,
664            timestamp: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
665            encryption_type: if self.password.is_some() {
666                HeaderEncryptionType::Aes256
667            } else {
668                HeaderEncryptionType::None
669            },
670            public: if self.public { 1u8 } else { 0u8 },
671            name: [0u8; 64],
672            reserved: [0u8; 64],
673        };
674
675        primary_header.name[0..self.name.len()].copy_from_slice(&self.name.clone().as_bytes()[..]);
676
677        // Prepare protected header
678        let mut protected_header = ProtectedHeader {
679            block_size: source.header.cluster_size() as u32,
680            cluster_count: source.count_clusters()? as u32,
681            cluster_compression: ClusterCompressionType::None, // TODO
682            cluster_encryption: if self.password.is_some() {
683                ClusterEncryptionType::Aes256
684            } else {
685                ClusterEncryptionType::None
686            },
687            cluster_key: rng.gen::<[u8; 32]>(),
688            nonce_count: 0,
689            nonce_table: vec![],
690        };
691
692        if self.password.is_some() {
693            protected_header.nonce_count = protected_header.cluster_count;
694            protected_header.nonce_table = (0..protected_header.cluster_count)
695                .map(|_| rng.gen::<[u8; 12]>())
696                .collect();
697        }
698
699        // Load the cluster cipher we just generated
700        let cluster_cipher =
701            Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&protected_header.cluster_key));
702
703        // Write primary header (we'll overwrite it at the end)
704        dest_file.seek(SeekFrom::Start(0))?;
705        primary_header.write(&mut dest_file)?;
706
707        // Write protected header
708        {
709            debug!(protected_header = ?protected_header, "Writing protected header");
710            let mut protected_header_bytes = Cursor::new(Vec::new());
711            protected_header.write(&mut protected_header_bytes)?;
712
713            let protected_header_bytes = match primary_header.encryption_type {
714                HeaderEncryptionType::None => protected_header_bytes.into_inner(),
715                HeaderEncryptionType::Aes256 => header_cipher.encrypt(
716                    Nonce::from_slice(&directory.protected_nonce),
717                    protected_header_bytes.into_inner()[..].as_ref(),
718                )?,
719            };
720
721            directory.protected_size = protected_header_bytes.len() as u32;
722            dest_file.write_all(&protected_header_bytes)?;
723        }
724
725        // Write config
726        {
727            let config_bytes = match primary_header.encryption_type {
728                HeaderEncryptionType::None => self.config.clone(),
729                HeaderEncryptionType::Aes256 => header_cipher.encrypt(
730                    Nonce::from_slice(&directory.config_nonce),
731                    self.config.as_ref(),
732                )?,
733            };
734
735            directory.config_offset = dest_file.stream_position()?;
736            directory.config_size = config_bytes.len() as u32;
737            dest_file.write_all(&config_bytes)?;
738        }
739
740        // Prepare the digest table
741        let mut digest_table = DigestTable {
742            digest_count: protected_header.cluster_count,
743            digest_table: vec![],
744        };
745
746        // Track the offset into the data
747        let mut block_offset: u64 = 0;
748
749        // Track cluster ordinal so we can lookup cluster nonces later
750        let mut cluster_count = 0;
751
752        // Track the cluster offset in the image file
753        let mut cluster_offset = dest_file.stream_position()?;
754
755        // Read from the qcow2 and write the clusters
756        for l1_entry in &source.l1_table {
757            if let Some(l2_table) = l1_entry.read_l2(&mut source_file, source.header.cluster_bits) {
758                for l2_entry in l2_table {
759                    if l2_entry.is_used {
760                        // Start building the cluster
761                        let mut cluster = Cluster {
762                            // The resulting size gets updated after we compress/encrypt
763                            size: 0,
764                            data: l2_entry.read_contents(
765                                &mut source_file,
766                                source.header.cluster_size(),
767                                source.header.compression_type,
768                            )?,
769                        };
770
771                        // TODO image size may exceed usize
772                        cluster
773                            .data
774                            .truncate((primary_header.size - block_offset) as usize);
775
776                        // Compute hash of the block which will be used when writing the block later
777                        let digest = Sha256::new().chain_update(&cluster.data).finalize();
778
779                        digest_table.digest_table.push(DigestTableEntry {
780                            digest: digest.into(),
781                            block_offset,
782                            cluster_offset,
783                        });
784
785                        // Perform compression
786                        cluster.data = match protected_header.cluster_compression {
787                            ClusterCompressionType::None => cluster.data,
788                            ClusterCompressionType::Zstd => {
789                                zstd::encode_all(std::io::Cursor::new(cluster.data), 0)?
790                            }
791                        };
792
793                        // Perform encryption
794                        cluster.data = match protected_header.cluster_encryption {
795                            ClusterEncryptionType::None => cluster.data,
796                            ClusterEncryptionType::Aes256 => cluster_cipher.encrypt(
797                                Nonce::from_slice(&protected_header.nonce_table[cluster_count]),
798                                cluster.data.as_ref(),
799                            )?,
800                        };
801
802                        cluster.size = cluster.data.len() as u32;
803
804                        // Write the cluster
805                        trace!(
806                            cluster_size = cluster.size,
807                            cluster_offset = cluster_offset,
808                            "Recording cluster",
809                        );
810                        cluster.write(&mut dest_file)?;
811
812                        // Advance offset
813                        cluster_offset += 4; // size
814                        cluster_offset += cluster.size as u64;
815                        cluster_count += 1;
816                    }
817                    block_offset += source.header.cluster_size();
818                    // self.progress(source.header.cluster_size(), source.header.size);
819                }
820            } else {
821                block_offset +=
822                    source.header.cluster_size() * source.header.l2_entries_per_cluster();
823                // self.progress(
824                //     source.header.cluster_size() * source.header.l2_entries_per_cluster(),
825                //     source.header.size,
826                // );
827            }
828        }
829
830        // Write the completed digest table
831        {
832            let mut digest_table_bytes = Cursor::new(Vec::new());
833            digest_table.write(&mut digest_table_bytes)?;
834
835            let digest_table_bytes = match primary_header.encryption_type {
836                HeaderEncryptionType::None => digest_table_bytes.into_inner(),
837                HeaderEncryptionType::Aes256 => header_cipher.encrypt(
838                    Nonce::from_slice(&directory.digest_table_nonce),
839                    digest_table_bytes.into_inner()[..].as_ref(),
840                )?,
841            };
842
843            directory.digest_table_offset = dest_file.stream_position()?;
844            directory.digest_table_size = digest_table_bytes.len() as u32;
845            dest_file.write_all(&digest_table_bytes)?;
846        }
847
848        // Write the completed directory
849        {
850            let mut directory_bytes = Cursor::new(Vec::new());
851            directory.write(&mut directory_bytes)?;
852
853            let directory_bytes = match primary_header.encryption_type {
854                HeaderEncryptionType::None => directory_bytes.into_inner(),
855                HeaderEncryptionType::Aes256 => header_cipher.encrypt(
856                    Nonce::from_slice(&primary_header.directory_nonce),
857                    directory_bytes.into_inner()[..].as_ref(),
858                )?,
859            };
860
861            primary_header.directory_offset = dest_file.stream_position()?;
862            primary_header.directory_size = directory_bytes.len() as u32;
863            dest_file.write_all(&directory_bytes)?;
864        }
865
866        // Write the completed primary header
867        dest_file.seek(SeekFrom::Start(0))?;
868        primary_header.write(&mut dest_file)?;
869
870        Ok(ImageHandle {
871            id: compute_id(&self.dest)?,
872            primary_header,
873            protected_header: Some(protected_header),
874            config: Some(self.config),
875            digest_table: Some(digest_table),
876            directory: Some(directory),
877            file_size: std::fs::metadata(&self.dest)?.len(),
878            path: self.dest,
879        })
880    }
881}
882
883#[cfg(test)]
884mod tests {
885    use std::process::Command;
886
887    use super::*;
888    use sha1::Sha1;
889    use test_log::test;
890
891    #[test]
892    fn convert_random_data() -> Result<()> {
893        let tmp = tempfile::tempdir()?;
894
895        // Generate file of random size and contents
896        let size = rand::thread_rng().gen_range(512..=1000);
897        let mut raw: Vec<u8> = Vec::new();
898        for _ in 0..size {
899            raw.push(rand::thread_rng().gen());
900        }
901
902        // Write out for qemu-img
903        std::fs::write(tmp.path().join("file.raw"), &raw)?;
904
905        // Convert with qemu-img
906        Command::new("qemu-img")
907            .arg("convert")
908            .arg("-f")
909            .arg("raw")
910            .arg("-O")
911            .arg("qcow2")
912            .arg(tmp.path().join("file.raw"))
913            .arg(tmp.path().join("file.qcow2"))
914            .spawn()?
915            .wait()?;
916
917        // Convert the qcow2 to gb
918        let image = ImageBuilder::new(tmp.path().join("file.gb"))
919            .convert(&Qcow3::open(tmp.path().join("file.qcow2"))?, size)?;
920
921        // Check raw content
922        image.write(tmp.path().join("output.raw"), |_, _| {})?;
923        assert_eq!(
924            std::fs::read(tmp.path().join("file.raw"))?,
925            std::fs::read(tmp.path().join("output.raw"))?,
926        );
927
928        Ok(())
929    }
930
931    #[test]
932    fn convert_small_qcow2_to_unencrypted_image() -> Result<()> {
933        let tmp = tempfile::tempdir()?;
934
935        // Convert the test qcow2
936        let image = ImageBuilder::new(tmp.path().join("small.gb"))
937            .convert(&Qcow3::open("test/small.qcow2")?, 4194304)?;
938
939        // Try to open the image we just converted
940        let mut loaded_image = ImageHandle::open(tmp.path().join("small.gb"))?;
941        assert_eq!(loaded_image.primary_header, image.primary_header);
942        assert_eq!(loaded_image.protected_header, image.protected_header);
943
944        // Try to load all sections
945        assert_eq!(loaded_image.digest_table, None);
946        loaded_image.load(None)?;
947        assert_eq!(loaded_image.digest_table.unwrap().digest_count, 2);
948
949        // Check raw content
950        image.write(tmp.path().join("small.raw"), |_, _| {})?;
951        assert_eq!(
952            hex::encode(
953                Sha1::new()
954                    .chain_update(&std::fs::read(tmp.path().join("small.raw"))?)
955                    .finalize()
956            ),
957            "34e1c79c80941e5519ec76433790191318a5c77b"
958        );
959
960        Ok(())
961    }
962
963    #[test]
964    fn convert_small_qcow2_to_encrypted_image() -> Result<()> {
965        let tmp = tempfile::tempdir()?;
966
967        // Convert the test qcow2
968        let image = ImageBuilder::new(tmp.path().join("small.gb"))
969            .password("1234")
970            .convert(&Qcow3::open("test/small.qcow2")?, 4194304)?;
971
972        // Try to open the image
973        let mut loaded_image = ImageHandle::open(tmp.path().join("small.gb"))?;
974        assert_eq!(loaded_image.primary_header, image.primary_header);
975        assert_eq!(loaded_image.protected_header, None);
976
977        // Try to load all sections
978        loaded_image.load(Some("1234".to_string()))?;
979
980        // Check raw content
981        image.write(tmp.path().join("small.raw"), |_, _| {})?;
982        assert_eq!(
983            hex::encode(
984                Sha1::new()
985                    .chain_update(&std::fs::read(tmp.path().join("small.raw"))?)
986                    .finalize()
987            ),
988            "34e1c79c80941e5519ec76433790191318a5c77b"
989        );
990
991        Ok(())
992    }
993}