1use serde::{Deserialize, Serialize};
2
3pub const PROTOCOL_MAJOR: u16 = 1;
4pub(crate) const MAX_FRAME_BYTES: usize = 64 * 1024;
5
6#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
7pub struct RequestId([u8; 16]);
8
9impl RequestId {
10 pub fn random() -> Self {
11 use rand::RngExt as _;
12
13 Self(rand::rng().random())
14 }
15}
16
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(rename_all = "kebab-case", tag = "method")]
19pub enum ApplicationRequest {
20 Status,
21 Touch,
22 ListCredentials,
23 DeleteCredential {
24 credential_id: String,
25 },
26 #[serde(other)]
27 Unknown,
28}
29
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "kebab-case", tag = "result")]
32pub enum ApplicationResponse {
33 Status(Status),
34 Touch(TouchReceipt),
35 Credentials { credentials: Vec<CredentialSummary> },
36 Deleted { credential_id: String },
37}
38
39#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
40pub struct Status {
41 pub product: String,
42 pub package: String,
43 pub version: String,
44 pub protocol_major: u16,
45 pub device_present: bool,
46 pub pending_touch: bool,
47 pub credential_count: usize,
48}
49
50#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct TouchReceipt {
52 pub operation: String,
53 pub rp_id: String,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57pub struct CredentialSummary {
58 pub credential_id: String,
59 pub rp_id: String,
60 pub user_name: Option<String>,
61 pub discoverable: bool,
62 pub backup_eligible: bool,
63 pub backed_up: bool,
64}
65
66#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[serde(rename_all = "kebab-case")]
68pub enum ErrorCode {
69 BadRequest,
70 Unauthorized,
71 UnsupportedProtocol,
72 NotFound,
73 Conflict,
74 Unavailable,
75 Internal,
76}
77
78#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
79pub struct ProtocolError {
80 pub code: ErrorCode,
81 pub message: String,
82}
83
84impl ProtocolError {
85 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
86 Self {
87 code,
88 message: message.into(),
89 }
90 }
91}
92
93#[derive(Debug, thiserror::Error)]
94pub enum ApplicationError {
95 #[error("auc application frame exceeds the {MAX_FRAME_BYTES}-byte limit")]
96 FrameTooLarge,
97 #[error("auc agent closed the connection before sending a complete frame")]
98 EarlyEof,
99 #[error("failed to encode auc application CBOR: {0}")]
100 Encode(String),
101 #[error("failed to decode auc application CBOR: {0}")]
102 Decode(String),
103 #[error("auc response request ID did not match the request")]
104 MismatchedRequestId,
105 #[error("auc application protocol v{0} is unsupported")]
106 UnsupportedProtocol(u16),
107 #[error("auc request failed ({code:?}): {message}")]
108 Remote { code: ErrorCode, message: String },
109 #[error("auc application I/O failed: {0}")]
110 Io(#[from] std::io::Error),
111}
112
113#[derive(Debug, Deserialize, Serialize)]
114pub(crate) struct RequestEnvelope {
115 pub request_id: RequestId,
116 pub minimum_protocol_major: u16,
117 pub maximum_protocol_major: u16,
118 pub request: ApplicationRequest,
119}
120
121#[derive(Debug, Deserialize, Serialize)]
122pub(crate) struct ResponseEnvelope {
123 pub request_id: RequestId,
124 pub protocol_major: u16,
125 pub body: ResponseBody,
126}
127
128#[derive(Debug, Deserialize, Serialize)]
129#[serde(rename_all = "kebab-case", tag = "status", content = "body")]
130pub(crate) enum ResponseBody {
131 Ok(ApplicationResponse),
132 Error(ProtocolError),
133}
134
135pub(crate) fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, ApplicationError> {
136 let mut bytes = Vec::new();
137 ciborium::into_writer(value, &mut bytes)
138 .map_err(|error| ApplicationError::Encode(error.to_string()))?;
139 if bytes.len() > MAX_FRAME_BYTES {
140 return Err(ApplicationError::FrameTooLarge);
141 }
142 Ok(bytes)
143}
144
145pub(crate) fn decode<T>(bytes: &[u8]) -> Result<T, ApplicationError>
146where
147 T: for<'de> Deserialize<'de>,
148{
149 if bytes.len() > MAX_FRAME_BYTES {
150 return Err(ApplicationError::FrameTooLarge);
151 }
152 ciborium::from_reader(bytes).map_err(|error| ApplicationError::Decode(error.to_string()))
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn requests_round_trip_and_frames_are_bounded() {
161 let envelope = RequestEnvelope {
162 request_id: RequestId([0xDE; 16]),
163 minimum_protocol_major: 1,
164 maximum_protocol_major: 1,
165 request: ApplicationRequest::DeleteCredential {
166 credential_id: "deadbeef".to_string(),
167 },
168 };
169 let decoded: RequestEnvelope = decode(&encode(&envelope).unwrap()).unwrap();
170 assert_eq!(decoded.request_id, envelope.request_id);
171 assert_eq!(decoded.request, envelope.request);
172 assert!(matches!(
173 decode::<RequestEnvelope>(&vec![0; MAX_FRAME_BYTES + 1]),
174 Err(ApplicationError::FrameTooLarge)
175 ));
176 }
177
178 #[test]
179 fn protocol_v1_ignores_additive_fields() {
180 let envelope = RequestEnvelope {
181 request_id: RequestId([0xDE; 16]),
182 minimum_protocol_major: PROTOCOL_MAJOR,
183 maximum_protocol_major: PROTOCOL_MAJOR,
184 request: ApplicationRequest::DeleteCredential {
185 credential_id: "deadbeef".to_string(),
186 },
187 };
188 let mut value: ciborium::Value =
189 ciborium::from_reader(encode(&envelope).unwrap().as_slice()).unwrap();
190 let ciborium::Value::Map(fields) = &mut value else {
191 panic!("application envelope must encode as a map");
192 };
193 fields.push((
194 ciborium::Value::Text("future-envelope-field".to_string()),
195 ciborium::Value::Bool(true),
196 ));
197 let request = fields
198 .iter_mut()
199 .find_map(|(key, value)| {
200 (key == &ciborium::Value::Text("request".to_string())).then_some(value)
201 })
202 .unwrap();
203 let ciborium::Value::Map(request_fields) = request else {
204 panic!("application method must encode as a map");
205 };
206 request_fields.push((
207 ciborium::Value::Text("future-method-field".to_string()),
208 ciborium::Value::Integer(0xDEADBEEF_u64.into()),
209 ));
210 let mut encoded = Vec::new();
211 ciborium::into_writer(&value, &mut encoded).unwrap();
212
213 let decoded: RequestEnvelope = decode(&encoded).unwrap();
214 assert_eq!(decoded.request, envelope.request);
215 }
216}