1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Encrypted header encoding.
//!
//! This module provides functions for encoding encrypted archive headers
//! using LZMA2 compression and AES-256 encryption.
#[cfg(feature = "aes")]
use std::io::{Seek, Write};
#[cfg(feature = "aes")]
use crate::format::property_id;
#[cfg(feature = "aes")]
use crate::format::reader::write_variable_u64;
#[cfg(feature = "aes")]
use crate::{Error, Result};
#[cfg(feature = "aes")]
use super::Writer;
/// Returns the smallest LZMA2 dictionary property that covers `size`.
///
/// The property encodes `(2 | (p & 1)) << (p / 2 + 11)`, so property 0 is 4 KiB
/// and each step up is the next half-power of two. A dictionary at least as
/// large as the data behaves exactly like a larger one, since no match can
/// reach further back than the data itself.
#[cfg(any(feature = "aes", feature = "lzma2"))]
pub(crate) fn lzma2_dictionary_property(size: u64) -> u8 {
(0u8..=40)
.find(|&p| {
let dictionary = u64::from(2 | (p & 1)) << (p / 2 + 11);
dictionary >= size
})
.unwrap_or(40)
}
#[cfg(feature = "aes")]
impl<W: Write + Seek> Writer<W> {
/// Returns this archive's salt and a fresh IV for one stream.
///
/// The salt is generated once and kept, so the key is derived once no matter
/// how many streams the archive has; the IV is new every time, which is what
/// CBC actually requires.
pub(crate) fn nonce_for_stream(&mut self) -> Result<(Vec<u8>, [u8; 16])> {
let policy = self.options.nonce_policy.clone();
self.nonce_for_stream_under(&policy)
}
/// Draws a nonce from the given policy rather than the live options.
///
/// A buffered entry is encrypted after the fact, and the policy is part of
/// what it was accepted under: reading the current one lost a deterministic
/// policy set for that entry, so two runs of the same programme produced
/// different archives.
pub(crate) fn nonce_for_stream_under(
&mut self,
policy: &crate::crypto::NoncePolicy,
) -> Result<(Vec<u8>, [u8; 16])> {
let salt = match &self.archive_salt {
Some(salt) => salt.clone(),
None => {
let (salt, _) = policy.generate()?;
self.archive_salt = Some(salt.clone());
salt
}
};
Ok((salt, policy.next_iv()?))
}
/// Encodes an encrypted header.
///
/// The header is compressed with LZMA2, encrypted with AES-256, and described
/// by an ENCODED_HEADER structure. The two are returned separately as
/// `(payload, structure)` because they occupy different places in the file:
/// the payload is an ordinary packed stream living in the data area, and the
/// structure is the archive's next header, which points back at it.
///
/// `pack_pos` is the payload's offset relative to the end of the signature
/// header, which is the base every `PackInfo` position is measured from.
/// Writing the payload inline after the structure and recording a position of
/// zero instead makes the archive unreadable by every other 7z implementation.
pub(crate) fn encode_encrypted_header(
&mut self,
plain_header: &[u8],
pack_pos: u64,
nonce: (Vec<u8>, [u8; 16]),
) -> Result<(Vec<u8>, Vec<u8>)> {
use crate::codec::method;
use crate::crypto::{Aes256Encoder, AesProperties, derive_key_cached};
let password =
self.options.password.clone().ok_or_else(|| {
Error::InvalidFormat("header encryption requires a password".into())
})?;
// Step 1: Compress the header with LZMA2
let compressed = {
use lzma_rust2::{Lzma2Options, Lzma2Writer};
let mut compressed = Vec::new();
let options = Lzma2Options::with_preset(5);
let mut encoder = Lzma2Writer::new(&mut compressed, options);
encoder.write_all(plain_header).map_err(Error::Io)?;
encoder.finish().map_err(Error::Io)?;
compressed
};
// Step 2: Encrypt the compressed data with AES-256
let (salt, iv) = nonce;
// Fixed here, beside the derivation, like the one for entry data: the
// header used to be encrypted with whatever password was set at
// `finish` while a buffered entry had already keyed the archive on
// another, producing an archive no single password opens.
self.hold_password(&password)?;
let cycles = self.options.nonce_policy.num_cycles_power();
let key = derive_key_cached(&password, &salt, cycles)?;
let encrypted = {
let mut output = Vec::new();
let mut encoder = Aes256Encoder::with_key_iv(&mut output, key, iv);
encoder.write_all(&compressed).map_err(Error::Io)?;
encoder.finish().map_err(Error::Io)?;
output
};
// Step 3: Build the ENCODED_HEADER structure
let mut encoded = Vec::new();
// ENCODED_HEADER marker
encoded.push(property_id::ENCODED_HEADER);
// PackInfo
encoded.push(property_id::PACK_INFO);
write_variable_u64(&mut encoded, pack_pos)?;
write_variable_u64(&mut encoded, 1)?; // num_pack_streams = 1
// Pack size
encoded.push(property_id::SIZE);
write_variable_u64(&mut encoded, encrypted.len() as u64)?;
encoded.push(property_id::END);
// UnpackInfo
encoded.push(property_id::UNPACK_INFO);
encoded.push(property_id::FOLDER);
write_variable_u64(&mut encoded, 1)?; // num_folders = 1
encoded.push(0); // external = 0 (inline)
// Folder with 2 coders: AES-256 (decryption) -> LZMA2 (decompression)
// 7z coder chain order: coders[1] applied first, coders[0] applied second
// For encrypted headers: packed -> AES decrypt (coder 1) -> LZMA2 decompress (coder 0) -> plain
//
// Number of coders
encoded.push(0x02);
// Coder 0: LZMA2 (outer - decompression, applied SECOND when reading)
//
// The declared dictionary is what a reader allocates before it can decode
// anything. A header is a few hundred bytes, and declaring a flat 16 MB
// made every reader of our encrypted archives reserve 16 MB to decode it.
let lzma2_props = [lzma2_dictionary_property(plain_header.len() as u64)];
// LZMA2 method ID: 0x21
let lzma2_flags = (method::LZMA2.len() as u8) | 0x20; // 1 byte + has properties
encoded.push(lzma2_flags);
encoded.extend_from_slice(method::LZMA2);
write_variable_u64(&mut encoded, lzma2_props.len() as u64)?;
encoded.extend_from_slice(&lzma2_props);
// Coder 1: AES-256 (inner - decryption, applied FIRST when reading)
let aes_props =
AesProperties::encode(self.options.nonce_policy.num_cycles_power(), &salt, &iv)?;
// AES method ID: 0x06, 0xF1, 0x07, 0x01
let aes_flags = (method::AES.len() as u8) | 0x20; // 4 bytes + has properties
encoded.push(aes_flags);
encoded.extend_from_slice(method::AES);
write_variable_u64(&mut encoded, aes_props.len() as u64)?;
encoded.extend_from_slice(&aes_props);
// BindPair: connect AES output (stream 1) to LZMA2 input (stream 0)
// For a 2-coder chain, we have 1 bind pair
// The inner coder (AES) output goes to the outer coder (LZMA2) input
write_variable_u64(&mut encoded, 0)?; // in_index (LZMA2's input, stream 0)
write_variable_u64(&mut encoded, 1)?; // out_index (AES's output, stream 1)
// Unpack sizes (for both coders' outputs)
// Coder 0 (LZMA2) output = final uncompressed size
// Coder 1 (AES) output = compressed size (LZMA2 output)
encoded.push(property_id::CODERS_UNPACK_SIZE);
write_variable_u64(&mut encoded, plain_header.len() as u64)?; // LZMA2 output = final size
write_variable_u64(&mut encoded, compressed.len() as u64)?; // AES output = compressed size
// CRC of the decoded header. 7-Zip writes it, and it is what lets a
// reader reject a wrong password before trying to parse the garbage that
// decrypting with it produces.
encoded.push(property_id::CRC);
encoded.push(1); // all defined
encoded.extend_from_slice(&crc32fast::hash(plain_header).to_le_bytes());
encoded.push(property_id::END); // End UnpackInfo
encoded.push(property_id::END); // End streams (no SubStreamsInfo needed)
Ok((encrypted, encoded))
}
}