1use alloc::vec::Vec;
4
5use pamoja_core::{Error, Result};
6use pamoja_security::Signature;
7use sha2::{Digest, Sha256};
8
9const HEADER_LEN: usize = 8 + 32 + 64;
11
12#[derive(Clone, Debug)]
20pub struct Entry {
21 index: u64,
22 prev: [u8; 32],
23 signature: Signature,
24 payload: Vec<u8>,
25}
26
27impl Entry {
28 pub(crate) fn new(index: u64, prev: [u8; 32], signature: Signature, payload: Vec<u8>) -> Self {
29 Self {
30 index,
31 prev,
32 signature,
33 payload,
34 }
35 }
36
37 pub fn index(&self) -> u64 {
43 self.index
44 }
45
46 pub fn previous(&self) -> [u8; 32] {
52 self.prev
53 }
54
55 pub fn payload(&self) -> &[u8] {
61 &self.payload
62 }
63
64 pub fn signature(&self) -> &Signature {
70 &self.signature
71 }
72
73 pub fn digest(&self) -> [u8; 32] {
80 digest(self.index, &self.prev, &self.payload)
81 }
82
83 pub fn to_bytes(&self) -> Vec<u8> {
92 let mut bytes = Vec::with_capacity(HEADER_LEN + self.payload.len());
93 bytes.extend_from_slice(&self.index.to_le_bytes());
94 bytes.extend_from_slice(&self.prev);
95 bytes.extend_from_slice(&self.signature.to_bytes());
96 bytes.extend_from_slice(&self.payload);
97 bytes
98 }
99
100 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
115 if bytes.len() < HEADER_LEN {
116 return Err(Error::Codec(
117 "audit entry is shorter than its header".into(),
118 ));
119 }
120 let index = u64::from_le_bytes(bytes[..8].try_into().expect("eight index bytes"));
121 let prev: [u8; 32] = bytes[8..40].try_into().expect("thirty-two previous bytes");
122 let signature: [u8; 64] = bytes[40..HEADER_LEN]
123 .try_into()
124 .expect("sixty-four signature bytes");
125 let payload = bytes[HEADER_LEN..].to_vec();
126 Ok(Self::new(
127 index,
128 prev,
129 Signature::from_bytes(&signature),
130 payload,
131 ))
132 }
133}
134
135pub(crate) fn digest(index: u64, prev: &[u8; 32], payload: &[u8]) -> [u8; 32] {
137 let mut hasher = Sha256::new();
138 hasher.update(index.to_le_bytes());
139 hasher.update(prev);
140 hasher.update(payload);
141 let out = hasher.finalize();
142 let mut digest = [0u8; 32];
143 digest.copy_from_slice(&out);
144 digest
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use pamoja_security::DeviceIdentity;
151
152 #[test]
153 fn an_entry_round_trips_through_bytes() {
154 let device = DeviceIdentity::from_seed(&[1u8; 32]);
155 let signature = device.sign(b"x");
156 let entry = Entry::new(3, [7u8; 32], signature, b"payload".to_vec());
157
158 let bytes = entry.to_bytes();
159 let restored = Entry::from_bytes(&bytes).expect("parse");
160
161 assert_eq!(restored.index(), 3);
162 assert_eq!(restored.previous(), [7u8; 32]);
163 assert_eq!(restored.payload(), b"payload");
164 assert_eq!(restored.to_bytes(), bytes);
165 }
166
167 #[test]
168 fn a_short_buffer_is_rejected() {
169 let result = Entry::from_bytes(&[0u8; 10]);
170 assert!(matches!(result, Err(Error::Codec(_))));
171 }
172}