1use std::collections::HashMap;
18
19use crate::{ffi, mem};
20
21#[derive(Debug, thiserror::Error)]
23pub enum CryptoError {
24 #[error("crypto: host returned no result")]
28 NoResult,
29
30 #[error("crypto: invalid base64 ciphertext")]
32 BadBase64,
33}
34
35pub fn hash(algorithm: &str, input: &str) -> Option<String> {
44 let args = serde_json::json!({
45 "algorithm": algorithm,
46 "input": input,
47 });
48 let args_str = args.to_string();
49 let (args_ptr, args_len) = mem::host_arg_str(&args_str);
50 let result = unsafe { ffi::crypto_hash(args_ptr, args_len) };
51 unsafe { mem::read_packed_string(result) }
53}
54
55pub fn hmac(secret_key: &str, input: &str) -> Option<String> {
65 let args = serde_json::json!({
66 "secret_key": secret_key,
67 "input": input,
68 });
69 let args_str = args.to_string();
70 let (args_ptr, args_len) = mem::host_arg_str(&args_str);
71 let result = unsafe { ffi::crypto_hmac(args_ptr, args_len) };
72 unsafe { mem::read_packed_string(result) }
74}
75
76pub fn sign_jwt(
86 algorithm: &str,
87 secret_key: &str,
88 claims: &HashMap<String, String>,
89) -> Option<String> {
90 let args = serde_json::json!({
91 "algorithm": algorithm,
92 "secret_key": secret_key,
93 "claims": claims,
94 });
95 let args_str = args.to_string();
96 let (args_ptr, args_len) = mem::host_arg_str(&args_str);
97 let result = unsafe { ffi::crypto_sign_jwt(args_ptr, args_len) };
98 unsafe { mem::read_packed_string(result) }
100}
101
102pub fn encrypt_aes(secret_key: &str, plaintext: &str) -> Result<String, CryptoError> {
107 let args = serde_json::json!({
108 "secret_key": secret_key,
109 "input": plaintext,
110 });
111 let args_str = args.to_string();
112 let (args_ptr, args_len) = mem::host_arg_str(&args_str);
113 let result = unsafe { ffi::crypto_encrypt_aes(args_ptr, args_len) };
114 unsafe { mem::read_packed_string(result) }.ok_or(CryptoError::NoResult)
116}
117
118pub fn decrypt_aes(secret_key: &str, ciphertext_b64: &str) -> Result<Vec<u8>, CryptoError> {
123 let args = serde_json::json!({
124 "secret_key": secret_key,
125 "input": ciphertext_b64,
126 });
127 let args_str = args.to_string();
128 let (args_ptr, args_len) = mem::host_arg_str(&args_str);
129 let result = unsafe { ffi::crypto_decrypt_aes(args_ptr, args_len) };
130 unsafe { mem::read_packed_bytes(result) }.ok_or(CryptoError::NoResult)
132}
133
134#[derive(Debug, thiserror::Error)]
136pub enum RandomBytesError {
137 #[error("random_bytes: host returned no data")]
139 NoData,
140
141 #[error("random_bytes: host returned malformed hex")]
144 BadHex,
145}
146
147pub fn random_bytes(length: u32) -> Result<Vec<u8>, RandomBytesError> {
156 let result = unsafe { ffi::crypto_random_bytes(length as i32) };
157 let hex_str = unsafe { mem::read_packed_string(result) }.ok_or(RandomBytesError::NoData)?;
159 mem::hex_decode(&hex_str).ok_or(RandomBytesError::BadHex)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::ffi::test_host;
166
167 fn parse_args(s: &str) -> serde_json::Value {
168 serde_json::from_str(s).expect("captured args must be valid JSON")
169 }
170
171 #[test]
172 fn hash_constructs_args_and_returns_response() {
173 test_host::reset();
174 test_host::with_mock(|m| {
175 m.crypto_hash_response = Some("abcdef1234".into());
176 });
177 let result = hash("sha256", "hello world").unwrap();
178 assert_eq!(result, "abcdef1234");
179
180 let captured = test_host::read_mock(|m| m.last_crypto_hash_args.clone()).unwrap();
181 let args = parse_args(&captured);
182 assert_eq!(args["algorithm"], "sha256");
183 assert_eq!(args["input"], "hello world");
184 }
185
186 #[test]
187 fn hash_returns_none_when_host_empty() {
188 test_host::reset();
189 assert!(hash("sha256", "x").is_none());
190 }
191
192 #[test]
193 fn hmac_constructs_args() {
194 test_host::reset();
195 test_host::with_mock(|m| {
196 m.crypto_hmac_response = Some("deadbeef".into());
197 });
198 let mac = hmac("signing-key", "payload").unwrap();
199 assert_eq!(mac, "deadbeef");
200
201 let captured = test_host::read_mock(|m| m.last_crypto_hmac_args.clone()).unwrap();
202 let args = parse_args(&captured);
203 assert_eq!(args["secret_key"], "signing-key");
204 assert_eq!(args["input"], "payload");
205 }
206
207 #[test]
208 fn sign_jwt_serializes_claims() {
209 test_host::reset();
210 test_host::with_mock(|m| {
211 m.crypto_sign_jwt_response = Some("h.p.s".into());
212 });
213 let mut claims = HashMap::new();
214 claims.insert("sub".to_string(), "user-42".to_string());
215 claims.insert("aud".to_string(), "api".to_string());
216
217 let jwt = sign_jwt("HS256", "session-key", &claims).unwrap();
218 assert_eq!(jwt, "h.p.s");
219
220 let captured = test_host::read_mock(|m| m.last_crypto_sign_jwt_args.clone()).unwrap();
221 let args = parse_args(&captured);
222 assert_eq!(args["algorithm"], "HS256");
223 assert_eq!(args["secret_key"], "session-key");
224 assert_eq!(args["claims"]["sub"], "user-42");
225 assert_eq!(args["claims"]["aud"], "api");
226 }
227
228 #[test]
229 fn encrypt_aes_returns_ciphertext() {
230 test_host::reset();
231 test_host::with_mock(|m| {
232 m.crypto_encrypt_aes_response = Some("base64ciphertext==".into());
233 });
234 let ct = encrypt_aes("key", "plaintext").unwrap();
235 assert_eq!(ct, "base64ciphertext==");
236
237 let captured = test_host::read_mock(|m| m.last_crypto_encrypt_aes_args.clone()).unwrap();
238 let args = parse_args(&captured);
239 assert_eq!(args["secret_key"], "key");
240 assert_eq!(args["input"], "plaintext");
241 }
242
243 #[test]
244 fn encrypt_aes_no_result_is_error() {
245 test_host::reset();
246 match encrypt_aes("key", "x").unwrap_err() {
247 CryptoError::NoResult => {}
248 other => panic!("expected NoResult, got {:?}", other),
249 }
250 }
251
252 #[test]
253 fn decrypt_aes_returns_plaintext_bytes() {
254 test_host::reset();
255 test_host::with_mock(|m| {
256 m.crypto_decrypt_aes_response = Some(b"plaintext".to_vec());
257 });
258 let pt = decrypt_aes("key", "base64ct").unwrap();
259 assert_eq!(pt, b"plaintext");
260 }
261
262 #[test]
263 fn decrypt_aes_no_result_is_error() {
264 test_host::reset();
265 match decrypt_aes("key", "x").unwrap_err() {
266 CryptoError::NoResult => {}
267 other => panic!("expected NoResult, got {:?}", other),
268 }
269 }
270
271 #[test]
272 fn random_bytes_decodes_hex() {
273 test_host::reset();
274 test_host::with_mock(|m| {
275 m.crypto_random_bytes_response = Some("deadbeef".into());
276 });
277 let bytes = random_bytes(4).unwrap();
278 assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
279 assert_eq!(
280 test_host::read_mock(|m| m.last_random_bytes_length),
281 Some(4)
282 );
283 }
284
285 #[test]
286 fn random_bytes_no_data_is_error() {
287 test_host::reset();
288 match random_bytes(8).unwrap_err() {
289 RandomBytesError::NoData => {}
290 other => panic!("expected NoData, got {:?}", other),
291 }
292 }
293
294 #[test]
295 fn random_bytes_bad_hex_is_error() {
296 test_host::reset();
297 test_host::with_mock(|m| {
298 m.crypto_random_bytes_response = Some("xxxx".into());
299 });
300 match random_bytes(2).unwrap_err() {
301 RandomBytesError::BadHex => {}
302 other => panic!("expected BadHex, got {:?}", other),
303 }
304 }
305}