void_crypto/
machine_token.rs1use serde::{Deserialize, Serialize};
17
18use crate::kdf::{RecipientSecretKey, SigningSecretKey};
19use crate::{decrypt, encrypt, CryptoResult};
20
21const AAD_MACHINE_TOKEN: &[u8] = b"void:machine-token:v1";
23
24#[derive(Serialize, Deserialize)]
26pub struct MachineToken {
27 pub version: u32,
29 pub signing_secret: Vec<u8>,
31 pub recipient_secret: Vec<u8>,
33 pub content_key: Vec<u8>,
35 pub source_commit: Vec<u8>,
37 pub repo_name: Option<String>,
39 pub expires_at: u64,
41}
42
43impl MachineToken {
44 pub const VERSION: u32 = 1;
45
46 pub fn new(
48 signing_secret: &SigningSecretKey,
49 recipient_secret: &RecipientSecretKey,
50 content_key: &[u8; 32],
51 source_commit: Vec<u8>,
52 repo_name: Option<String>,
53 expires_at: u64,
54 ) -> Self {
55 Self {
56 version: Self::VERSION,
57 signing_secret: signing_secret.as_bytes().to_vec(),
58 recipient_secret: recipient_secret.as_bytes().to_vec(),
59 content_key: content_key.to_vec(),
60 source_commit,
61 repo_name,
62 expires_at,
63 }
64 }
65
66 pub fn is_expired(&self, now: u64) -> bool {
68 now > self.expires_at
69 }
70
71 pub fn to_bytes(&self) -> CryptoResult<Vec<u8>> {
73 let mut buf = Vec::new();
74 ciborium::into_writer(self, &mut buf)
75 .map_err(|e| crate::CryptoError::Serialization(e.to_string()))?;
76 Ok(buf)
77 }
78
79 pub fn from_bytes(data: &[u8]) -> CryptoResult<Self> {
81 ciborium::from_reader(data)
82 .map_err(|e| crate::CryptoError::Serialization(e.to_string()))
83 }
84
85 pub fn seal(&self, key: &[u8; 32]) -> CryptoResult<Vec<u8>> {
90 let plaintext = self.to_bytes()?;
91 encrypt(key, &plaintext, AAD_MACHINE_TOKEN)
92 }
93
94 pub fn unseal(ciphertext: &[u8], key: &[u8; 32]) -> CryptoResult<Self> {
96 let plaintext = decrypt(key, ciphertext, AAD_MACHINE_TOKEN)?;
97 Self::from_bytes(&plaintext)
98 }
99
100 pub fn to_base64(&self) -> CryptoResult<String> {
102 use base64::Engine;
103 let bytes = self.to_bytes()?;
104 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&bytes))
105 }
106
107 pub fn to_vault(&self) -> CryptoResult<crate::vault::KeyVault> {
112 let key_bytes: [u8; 32] = self.content_key.clone().try_into()
113 .map_err(|_| crate::CryptoError::InvalidKey("content key must be 32 bytes".into()))?;
114 let ck = crate::kdf::ContentKey::new(key_bytes);
115 Ok(crate::vault::KeyVault::from_content_key(ck))
116 }
117
118 pub fn to_signing_key(&self) -> CryptoResult<ed25519_dalek::SigningKey> {
120 let key_bytes: [u8; 32] = self.signing_secret.clone().try_into()
121 .map_err(|_| crate::CryptoError::InvalidKey("signing key must be 32 bytes".into()))?;
122 Ok(ed25519_dalek::SigningKey::from_bytes(&key_bytes))
123 }
124
125 pub fn from_base64(s: &str) -> CryptoResult<Self> {
127 use base64::Engine;
128 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
129 .decode(s)
130 .map_err(|e| crate::CryptoError::Serialization(e.to_string()))?;
131 Self::from_bytes(&bytes)
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::kdf::{RecipientSecretKey, SigningSecretKey};
139
140 fn make_test_token() -> MachineToken {
141 let signing = SigningSecretKey::from_bytes([0x42u8; 32]);
142 let recipient = RecipientSecretKey::from_bytes([0x43u8; 32]);
143 let content_key = [0x44u8; 32];
144 let commit_cid = vec![1, 2, 3, 4, 5];
145
146 MachineToken::new(
147 &signing,
148 &recipient,
149 &content_key,
150 commit_cid,
151 Some("test-repo".to_string()),
152 1700000000,
153 )
154 }
155
156 #[test]
157 fn roundtrip_cbor() {
158 let token = make_test_token();
159 let bytes = token.to_bytes().unwrap();
160 let decoded = MachineToken::from_bytes(&bytes).unwrap();
161
162 assert_eq!(decoded.version, MachineToken::VERSION);
163 assert_eq!(decoded.signing_secret, token.signing_secret);
164 assert_eq!(decoded.recipient_secret, token.recipient_secret);
165 assert_eq!(decoded.content_key, token.content_key);
166 assert_eq!(decoded.source_commit, token.source_commit);
167 assert_eq!(decoded.repo_name, Some("test-repo".to_string()));
168 assert_eq!(decoded.expires_at, 1700000000);
169 }
170
171 #[test]
172 fn roundtrip_base64() {
173 let token = make_test_token();
174 let encoded = token.to_base64().unwrap();
175 let decoded = MachineToken::from_base64(&encoded).unwrap();
176
177 assert_eq!(decoded.signing_secret, token.signing_secret);
178 assert_eq!(decoded.content_key, token.content_key);
179 }
180
181 #[test]
182 fn roundtrip_sealed() {
183 let token = make_test_token();
184 let seal_key = [0xAA; 32];
185
186 let sealed = token.seal(&seal_key).unwrap();
187 let unsealed = MachineToken::unseal(&sealed, &seal_key).unwrap();
188
189 assert_eq!(unsealed.signing_secret, token.signing_secret);
190 assert_eq!(unsealed.content_key, token.content_key);
191 }
192
193 #[test]
194 fn sealed_fails_with_wrong_key() {
195 let token = make_test_token();
196 let seal_key = [0xAA; 32];
197 let wrong_key = [0xBB; 32];
198
199 let sealed = token.seal(&seal_key).unwrap();
200 let result = MachineToken::unseal(&sealed, &wrong_key);
201
202 assert!(result.is_err());
203 }
204
205 #[test]
206 fn expiry_check() {
207 let token = make_test_token();
208 assert!(!token.is_expired(1699999999)); assert!(token.is_expired(1700000001)); }
211}