Skip to main content

keepass/format/
hmac_block_stream.rs

1use byteorder::{ByteOrder, LittleEndian};
2use hex_literal::hex;
3use hybrid_array::{typenum::U64, Array as GenericArray};
4use thiserror::Error;
5
6pub const HMAC_KEY_END: [u8; 1] = hex!("01");
7
8/// Read from a HMAC block stream into a raw buffer
9pub(crate) fn read_hmac_block_stream(
10    data: &[u8],
11    key: &GenericArray<u8, U64>,
12) -> Result<Vec<u8>, BlockStreamError> {
13    // keepassxc src/streams/HmacBlockStream.cpp
14
15    let mut out = Vec::new();
16
17    let mut pos = 0;
18    let mut block_index: u64 = 0;
19
20    while pos < data.len() {
21        let hmac = data.get(pos..(pos + 32)).ok_or(BlockStreamError::Eof)?;
22        let size_bytes = data.get((pos + 32)..(pos + 36)).ok_or(BlockStreamError::Eof)?;
23        let size = LittleEndian::read_u32(size_bytes) as usize;
24        let block = data
25            .get((pos + 36)..(pos + 36 + size))
26            .ok_or(BlockStreamError::Eof)?;
27
28        // verify block hmac
29        let hmac_block_key = get_hmac_block_key(block_index, key);
30        let mut block_index_buf = [0u8; 8];
31        LittleEndian::write_u64(&mut block_index_buf, block_index);
32
33        #[allow(clippy::expect_used)] // Block stream key is always correctly sized, so this can't fail
34        if hmac
35            != crate::crypt::calculate_hmac(&[&block_index_buf, size_bytes, block], &hmac_block_key)
36                .expect("Block stream key always correctly sized")
37                .as_slice()
38        {
39            return Err(BlockStreamError::BlockHashMismatch { block_index });
40        }
41
42        pos += 36 + size;
43        block_index += 1;
44
45        if size == 0 {
46            break;
47        }
48
49        out.extend_from_slice(block);
50    }
51
52    Ok(out)
53}
54
55#[cfg(feature = "save_kdbx4")]
56/// Write a raw buffer as a HMAC block stream
57pub(crate) fn write_hmac_block_stream(data: &[u8], key: &GenericArray<u8, U64>) -> Vec<u8> {
58    let mut out = Vec::new();
59
60    let mut pos = 0;
61    let mut block_index = 0;
62
63    while pos < data.len() {
64        let size = data.len() - pos;
65
66        #[allow(clippy::indexing_slicing)] // we check slice length at the beginning of the loop
67        let block = &data[pos..(pos + size)];
68
69        let mut size_bytes: Vec<u8> = vec![0; 4];
70        LittleEndian::write_u32(&mut size_bytes, size as u32);
71
72        // Generate block hmac
73        let hmac_block_key = get_hmac_block_key(block_index, key);
74        let mut block_index_buf = [0u8; 8];
75        LittleEndian::write_u64(&mut block_index_buf, block_index);
76
77        #[allow(clippy::expect_used)] // Block stream key is always correctly sized, so this can't fail
78        let hmac = crate::crypt::calculate_hmac(&[&block_index_buf, &size_bytes, block], &hmac_block_key)
79            .expect("Block stream key always correctly sized");
80
81        pos += 36 + size;
82        block_index += 1;
83
84        out.extend_from_slice(&hmac);
85        out.extend_from_slice(&size_bytes);
86        out.extend_from_slice(block);
87    }
88
89    // the end of the HMAC block stream should be an empty block, but with a valid HMAC
90    let hmac_block_key = get_hmac_block_key(block_index, key);
91    let mut block_index_buf = [0u8; 8];
92    LittleEndian::write_u64(&mut block_index_buf, block_index);
93
94    let size_bytes = vec![0; 4];
95
96    #[allow(clippy::expect_used)] // Block stream key is always correctly sized, so this can't fail
97    let hmac = crate::crypt::calculate_hmac(&[&block_index_buf, &size_bytes, &[]], &hmac_block_key)
98        .expect("Block stream key always correctly sized");
99
100    out.extend_from_slice(&hmac);
101    out.extend_from_slice(&size_bytes);
102
103    out
104}
105
106pub(crate) fn get_hmac_block_key(block_index: u64, key: &GenericArray<u8, U64>) -> GenericArray<u8, U64> {
107    let mut buf = [0u8; 8];
108    LittleEndian::write_u64(&mut buf, block_index);
109    crate::crypt::calculate_sha512(&[&buf, key])
110}
111
112/// Errors reading from the HMAC block stream
113#[derive(Debug, Error)]
114#[non_exhaustive]
115pub enum BlockStreamError {
116    /// The HMAC of a block did not match the expected value, indicating that the data may be
117    /// corrupted or tampered with.
118    #[error("Block hash mismatch for block {}", block_index)]
119    BlockHashMismatch {
120        /// The index of the block that failed the HMAC verification
121        block_index: u64,
122    },
123
124    /// The end of the file was reached unexpectedly while reading a block, indicating that the
125    /// data may be incomplete or corrupted.
126    #[error("unexpected end of file")]
127    Eof,
128}