1use serde::{Deserialize, Serialize};
8
9use crate::keys::{self, UserKeypair};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct ChangesetEnvelope {
13 pub device_id: String,
14 pub seq: u64,
15 pub schema_version: u32,
16 pub message: String,
17 pub timestamp: String,
18 pub changeset_size: usize,
19 #[serde(skip_serializing_if = "Option::is_none", default)]
21 pub author_pubkey: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none", default)]
25 pub signature: Option<String>,
26}
27
28#[derive(Serialize)]
32struct SignedEnvelopeFields<'a> {
33 device_id: &'a str,
34 seq: u64,
35 schema_version: u32,
36 message: &'a str,
37 timestamp: &'a str,
38 changeset_size: usize,
39}
40
41fn signing_payload(env: &ChangesetEnvelope, changeset_bytes: &[u8]) -> Vec<u8> {
46 let fields = SignedEnvelopeFields {
47 device_id: &env.device_id,
48 seq: env.seq,
49 schema_version: env.schema_version,
50 message: &env.message,
51 timestamp: &env.timestamp,
52 changeset_size: env.changeset_size,
53 };
54 let mut payload = serde_json::to_vec(&fields).expect("signed fields serialization cannot fail");
55 payload.push(0);
56 payload.extend_from_slice(changeset_bytes);
57 payload
58}
59
60pub fn sign_envelope(env: &mut ChangesetEnvelope, keypair: &UserKeypair, changeset_bytes: &[u8]) {
66 let sig = keypair.sign(&signing_payload(env, changeset_bytes));
67 env.author_pubkey = Some(hex::encode(keypair.public_key));
68 env.signature = Some(hex::encode(sig));
69}
70
71pub fn verify_changeset_signature(env: &ChangesetEnvelope, changeset_bytes: &[u8]) -> bool {
80 match (&env.author_pubkey, &env.signature) {
81 (None, None) => return true, (Some(_), None) | (None, Some(_)) => return false, _ => {}
84 }
85 let (Some(pk_hex), Some(sig_hex)) = (&env.author_pubkey, &env.signature) else {
86 unreachable!()
87 };
88
89 let Ok(pk_bytes) = hex::decode(pk_hex) else {
90 return false;
91 };
92 let Ok(sig_bytes) = hex::decode(sig_hex) else {
93 return false;
94 };
95
96 let Ok(pk): Result<[u8; keys::SIGN_PUBLICKEYBYTES], _> = pk_bytes.try_into() else {
97 return false;
98 };
99 let Ok(sig): Result<[u8; keys::SIGN_BYTES], _> = sig_bytes.try_into() else {
100 return false;
101 };
102
103 keys::verify_signature(&sig, &signing_payload(env, changeset_bytes), &pk)
104}
105
106pub fn pack(envelope: &ChangesetEnvelope, changeset: &[u8]) -> Vec<u8> {
110 let json = serde_json::to_vec(envelope).expect("envelope serialization cannot fail");
111 let mut buf = Vec::with_capacity(json.len() + 1 + changeset.len());
112 buf.extend_from_slice(&json);
113 buf.push(0);
114 buf.extend_from_slice(changeset);
115 buf
116}
117
118#[derive(Debug)]
120pub enum UnpackError {
121 NoSeparator,
123 InvalidJson(serde_json::Error),
125}
126
127impl std::fmt::Display for UnpackError {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 UnpackError::NoSeparator => write!(f, "no null separator in envelope"),
131 UnpackError::InvalidJson(e) => write!(f, "invalid envelope JSON: {e}"),
132 }
133 }
134}
135
136impl std::error::Error for UnpackError {}
137
138pub fn unpack(data: &[u8]) -> Result<(ChangesetEnvelope, Vec<u8>), UnpackError> {
140 let separator = data
145 .iter()
146 .position(|&b| b == 0)
147 .ok_or(UnpackError::NoSeparator)?;
148 let json_bytes = &data[..separator];
149 let changeset_bytes = &data[separator + 1..];
150
151 let envelope: ChangesetEnvelope =
152 serde_json::from_slice(json_bytes).map_err(UnpackError::InvalidJson)?;
153 Ok((envelope, changeset_bytes.to_vec()))
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::keys::KeyService;
160
161 fn test_envelope() -> ChangesetEnvelope {
162 ChangesetEnvelope {
163 device_id: "dev-abc123".into(),
164 seq: 42,
165 schema_version: 2,
166 message: "Imported Album One".into(),
167 timestamp: "2026-02-10T14:30:00Z".into(),
168 changeset_size: 4096,
169 author_pubkey: None,
170 signature: None,
171 }
172 }
173
174 #[test]
175 fn pack_unpack_roundtrip() {
176 let envelope = test_envelope();
177 let changeset = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01, 0x02];
178
179 let packed = pack(&envelope, &changeset);
180 let (unpacked_env, unpacked_cs) = unpack(&packed).expect("unpack");
181
182 assert_eq!(unpacked_env, envelope);
183 assert_eq!(unpacked_cs, changeset);
184 }
185
186 #[test]
187 fn pack_unpack_empty_changeset() {
188 let envelope = test_envelope();
189 let changeset: Vec<u8> = vec![];
190
191 let packed = pack(&envelope, &changeset);
192 let (unpacked_env, unpacked_cs) = unpack(&packed).expect("unpack");
193
194 assert_eq!(unpacked_env, envelope);
195 assert!(unpacked_cs.is_empty());
196 }
197
198 #[test]
199 fn pack_contains_null_separator() {
200 let envelope = test_envelope();
201 let changeset = vec![0xFF];
202
203 let packed = pack(&envelope, &changeset);
204
205 let null_positions: Vec<usize> = packed
207 .iter()
208 .enumerate()
209 .filter(|&(_, &b)| b == 0)
210 .map(|(i, _)| i)
211 .collect();
212
213 assert_eq!(null_positions.len(), 1);
215 }
216
217 #[test]
218 fn changeset_with_embedded_nulls() {
219 let envelope = test_envelope();
220 let changeset = vec![0x00, 0x00, 0xFF, 0x00];
223
224 let packed = pack(&envelope, &changeset);
225 let (unpacked_env, unpacked_cs) = unpack(&packed).expect("unpack");
226
227 assert_eq!(unpacked_env, envelope);
228 assert_eq!(unpacked_cs, changeset);
229 }
230
231 #[test]
232 fn unpack_invalid_no_separator() {
233 let data = b"hello world";
234 assert!(matches!(unpack(data), Err(UnpackError::NoSeparator)));
235 }
236
237 #[test]
238 fn unpack_invalid_bad_json() {
239 let mut data = b"not json".to_vec();
241 data.push(0);
242 data.extend_from_slice(b"changeset");
243
244 assert!(matches!(unpack(&data), Err(UnpackError::InvalidJson(_))));
245 }
246
247 #[test]
248 fn unpack_empty_input() {
249 assert!(matches!(unpack(&[]), Err(UnpackError::NoSeparator)));
250 }
251
252 #[test]
257 fn changeset_signing() {
258 std::env::remove_var("BAE_USER_SIGNING_KEY");
260 std::env::remove_var("BAE_USER_PUBLIC_KEY");
261
262 let ks = KeyService::new(true, "test-signing".to_string());
263 let keypair = ks.get_or_create_user_keypair().unwrap();
264
265 let changeset_bytes = b"some changeset payload";
266
267 let mut env = test_envelope();
269 sign_envelope(&mut env, &keypair, changeset_bytes);
270
271 assert!(env.author_pubkey.is_some());
272 assert!(env.signature.is_some());
273 assert_eq!(
274 env.author_pubkey.as_ref().unwrap(),
275 &hex::encode(keypair.public_key)
276 );
277 assert!(verify_changeset_signature(&env, changeset_bytes));
278
279 let packed = pack(&env, changeset_bytes);
281 let (unpacked_env, unpacked_cs) = unpack(&packed).expect("unpack");
282 assert_eq!(unpacked_env.author_pubkey, env.author_pubkey);
283 assert_eq!(unpacked_env.signature, env.signature);
284 assert!(verify_changeset_signature(&unpacked_env, &unpacked_cs));
285
286 assert!(!verify_changeset_signature(&env, b"tampered payload"));
288
289 let mut backdated = env.clone();
295 backdated.timestamp = "2000-01-01T00:00:00Z".to_string();
296 assert!(!verify_changeset_signature(&backdated, changeset_bytes));
297
298 let mut moved = env.clone();
301 moved.seq += 1;
302 assert!(!verify_changeset_signature(&moved, changeset_bytes));
303 let mut rehomed = env.clone();
304 rehomed.device_id = "other-device".to_string();
305 assert!(!verify_changeset_signature(&rehomed, changeset_bytes));
306
307 let unsigned_env = test_envelope();
309 assert!(verify_changeset_signature(&unsigned_env, changeset_bytes));
310
311 let mut bad_sig_env = env.clone();
313 bad_sig_env.signature = Some("not-valid-hex!!".to_string());
314 assert!(!verify_changeset_signature(&bad_sig_env, changeset_bytes));
315
316 let mut bad_pk_env = env.clone();
318 bad_pk_env.author_pubkey = Some(hex::encode([0u8; 16])); assert!(!verify_changeset_signature(&bad_pk_env, changeset_bytes));
320
321 std::env::remove_var("BAE_USER_SIGNING_KEY");
323 std::env::remove_var("BAE_USER_PUBLIC_KEY");
324 }
325}