a simple rust terminal ui (tui) for setting up alternative plc rotation keys driven by: secure enclave hardware (not synced) or software-based keys (synced to icloud)
plc
secure-enclave
touchid
icloud
atproto
1use anyhow::{Result, bail};
2
3/// Multicodec varint prefixes
4const P256_MULTICODEC: [u8; 2] = [0x80, 0x24]; // varint of 0x1200
5const K256_MULTICODEC: [u8; 2] = [0xe7, 0x01]; // varint of 0xe7
6
7#[derive(Debug, Clone, PartialEq)]
8pub enum KeyType {
9 P256,
10 K256,
11}
12
13/// Compress an uncompressed P-256 public key (65 bytes: 04 || x || y) to 33 bytes (02/03 || x).
14pub fn compress_p256_pubkey(uncompressed: &[u8]) -> Result<Vec<u8>> {
15 if uncompressed.len() == 33 && (uncompressed[0] == 0x02 || uncompressed[0] == 0x03) {
16 // Already compressed
17 return Ok(uncompressed.to_vec());
18 }
19
20 if uncompressed.len() != 65 || uncompressed[0] != 0x04 {
21 bail!(
22 "Expected uncompressed P-256 key (65 bytes starting with 0x04), got {} bytes",
23 uncompressed.len()
24 );
25 }
26
27 let x = &uncompressed[1..33];
28 let y = &uncompressed[33..65];
29
30 // If y is even, prefix is 0x02; if odd, prefix is 0x03
31 let prefix = if y[31] & 1 == 0 { 0x02 } else { 0x03 };
32
33 let mut compressed = Vec::with_capacity(33);
34 compressed.push(prefix);
35 compressed.extend_from_slice(x);
36 Ok(compressed)
37}
38
39/// Encode a P-256 public key as a did:key string.
40/// Accepts either uncompressed (65 bytes) or compressed (33 bytes) format.
41pub fn encode_p256_didkey(pub_key: &[u8]) -> Result<String> {
42 let compressed = compress_p256_pubkey(pub_key)?;
43
44 // Prepend multicodec varint for P-256
45 let mut prefixed = Vec::with_capacity(2 + compressed.len());
46 prefixed.extend_from_slice(&P256_MULTICODEC);
47 prefixed.extend_from_slice(&compressed);
48
49 // Base58btc encode with 'z' multibase prefix
50 let encoded = bs58::encode(&prefixed).into_string();
51
52 Ok(format!("did:key:z{}", encoded))
53}
54
55/// Decode a did:key string back to its raw public key bytes and key type.
56pub fn decode_didkey(did_key: &str) -> Result<(Vec<u8>, KeyType)> {
57 let stripped = did_key
58 .strip_prefix("did:key:z")
59 .ok_or_else(|| anyhow::anyhow!("Invalid did:key format: must start with 'did:key:z'"))?;
60
61 let decoded = bs58::decode(stripped).into_vec()?;
62
63 if decoded.len() < 2 {
64 bail!("did:key payload too short");
65 }
66
67 if decoded[0] == P256_MULTICODEC[0] && decoded[1] == P256_MULTICODEC[1] {
68 let key_bytes = decoded[2..].to_vec();
69 if key_bytes.len() != 33 {
70 bail!("P-256 compressed key should be 33 bytes, got {}", key_bytes.len());
71 }
72 Ok((key_bytes, KeyType::P256))
73 } else if decoded[0] == K256_MULTICODEC[0] && decoded[1] == K256_MULTICODEC[1] {
74 let key_bytes = decoded[2..].to_vec();
75 if key_bytes.len() != 33 {
76 bail!("K-256 compressed key should be 33 bytes, got {}", key_bytes.len());
77 }
78 Ok((key_bytes, KeyType::K256))
79 } else {
80 bail!(
81 "Unknown multicodec prefix: 0x{:02x} 0x{:02x}",
82 decoded[0],
83 decoded[1]
84 );
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_compress_already_compressed() {
94 let mut compressed = vec![0x02];
95 compressed.extend_from_slice(&[0xaa; 32]);
96 let result = compress_p256_pubkey(&compressed).unwrap();
97 assert_eq!(result, compressed);
98 }
99
100 #[test]
101 fn test_compress_uncompressed_even_y() {
102 let mut uncompressed = vec![0x04];
103 uncompressed.extend_from_slice(&[0xab; 32]); // x
104 let mut y = vec![0xcd; 32];
105 y[31] = 0x02; // even
106 uncompressed.extend_from_slice(&y);
107
108 let result = compress_p256_pubkey(&uncompressed).unwrap();
109 assert_eq!(result.len(), 33);
110 assert_eq!(result[0], 0x02); // even y -> 0x02
111 }
112
113 #[test]
114 fn test_compress_uncompressed_odd_y() {
115 let mut uncompressed = vec![0x04];
116 uncompressed.extend_from_slice(&[0xab; 32]); // x
117 let mut y = vec![0xcd; 32];
118 y[31] = 0x03; // odd
119 uncompressed.extend_from_slice(&y);
120
121 let result = compress_p256_pubkey(&uncompressed).unwrap();
122 assert_eq!(result.len(), 33);
123 assert_eq!(result[0], 0x03); // odd y -> 0x03
124 }
125
126 #[test]
127 fn test_encode_decode_roundtrip() {
128 // Generate a fake compressed P-256 key
129 let mut compressed = vec![0x02];
130 compressed.extend_from_slice(&[0x42; 32]);
131
132 let did_key = encode_p256_didkey(&compressed).unwrap();
133 assert!(did_key.starts_with("did:key:zDnae"));
134
135 let (decoded, key_type) = decode_didkey(&did_key).unwrap();
136 assert_eq!(key_type, KeyType::P256);
137 assert_eq!(decoded, compressed);
138 }
139
140 #[test]
141 fn test_encode_from_uncompressed() {
142 let mut uncompressed = vec![0x04];
143 uncompressed.extend_from_slice(&[0x42; 32]); // x
144 let mut y = vec![0x43; 32];
145 y[31] = 0x00; // even
146 uncompressed.extend_from_slice(&y);
147
148 let did_key = encode_p256_didkey(&uncompressed).unwrap();
149 assert!(did_key.starts_with("did:key:zDnae"));
150
151 let (decoded, key_type) = decode_didkey(&did_key).unwrap();
152 assert_eq!(key_type, KeyType::P256);
153 assert_eq!(decoded.len(), 33);
154 assert_eq!(decoded[0], 0x02); // even y
155 }
156
157 #[test]
158 fn test_decode_invalid_prefix() {
159 let result = decode_didkey("did:key:zInvalidKey");
160 assert!(result.is_err());
161 }
162
163 #[test]
164 fn test_decode_k256() {
165 // Construct a valid K-256 did:key
166 let mut payload = Vec::new();
167 payload.extend_from_slice(&K256_MULTICODEC);
168 let mut key = vec![0x02];
169 key.extend_from_slice(&[0x55; 32]);
170 payload.extend_from_slice(&key);
171
172 let encoded = bs58::encode(&payload).into_string();
173 let did_key = format!("did:key:z{}", encoded);
174
175 let (decoded, key_type) = decode_didkey(&did_key).unwrap();
176 assert_eq!(key_type, KeyType::K256);
177 assert_eq!(decoded, key);
178 }
179
180 // Known test vector: a well-known P-256 did:key
181 #[test]
182 fn test_known_p256_didkey_prefix() {
183 // All P-256 did:keys start with "did:key:zDnae"
184 let mut compressed = vec![0x03];
185 compressed.extend_from_slice(&[0x00; 32]);
186 let did_key = encode_p256_didkey(&compressed).unwrap();
187 assert!(did_key.starts_with("did:key:zDnae"), "P-256 did:key should start with 'zDnae', got: {}", did_key);
188 }
189
190 // --- Additional tests ---
191
192 #[test]
193 fn test_compress_invalid_length() {
194 // Too short
195 let result = compress_p256_pubkey(&[0x04, 0x01, 0x02]);
196 assert!(result.is_err());
197
198 // Wrong prefix
199 let mut bad = vec![0x05];
200 bad.extend_from_slice(&[0x00; 64]);
201 let result = compress_p256_pubkey(&bad);
202 assert!(result.is_err());
203 }
204
205 #[test]
206 fn test_compress_preserves_x_coordinate() {
207 let mut uncompressed = vec![0x04];
208 let x: Vec<u8> = (0..32).collect();
209 let mut y = vec![0x00; 32];
210 y[31] = 0x04; // even
211 uncompressed.extend_from_slice(&x);
212 uncompressed.extend_from_slice(&y);
213
214 let compressed = compress_p256_pubkey(&uncompressed).unwrap();
215 assert_eq!(&compressed[1..], &x[..]);
216 }
217
218 #[test]
219 fn test_compress_03_prefix_passthrough() {
220 let mut compressed = vec![0x03];
221 compressed.extend_from_slice(&[0xbb; 32]);
222 let result = compress_p256_pubkey(&compressed).unwrap();
223 assert_eq!(result, compressed);
224 }
225
226 #[test]
227 fn test_decode_missing_did_key_prefix() {
228 assert!(decode_didkey("zDnae123").is_err());
229 assert!(decode_didkey("did:web:example.com").is_err());
230 assert!(decode_didkey("").is_err());
231 }
232
233 #[test]
234 fn test_decode_too_short_payload() {
235 // Valid base58 but only 1 byte after decoding
236 let encoded = bs58::encode(&[0x80]).into_string();
237 let result = decode_didkey(&format!("did:key:z{}", encoded));
238 assert!(result.is_err());
239 }
240
241 #[test]
242 fn test_decode_wrong_key_length() {
243 // P256 prefix but wrong key length (only 10 bytes instead of 33)
244 let mut payload = Vec::new();
245 payload.extend_from_slice(&P256_MULTICODEC);
246 payload.extend_from_slice(&[0x02; 10]); // too short
247
248 let encoded = bs58::encode(&payload).into_string();
249 let result = decode_didkey(&format!("did:key:z{}", encoded));
250 assert!(result.is_err());
251 }
252
253 #[test]
254 fn test_roundtrip_multiple_keys() {
255 // Test with several different key values
256 for prefix_byte in [0x02u8, 0x03] {
257 for fill in [0x00u8, 0x42, 0xFF] {
258 let mut compressed = vec![prefix_byte];
259 compressed.extend_from_slice(&[fill; 32]);
260
261 let did_key = encode_p256_didkey(&compressed).unwrap();
262 let (decoded, key_type) = decode_didkey(&did_key).unwrap();
263 assert_eq!(key_type, KeyType::P256);
264 assert_eq!(decoded, compressed, "Roundtrip failed for prefix={:#04x} fill={:#04x}", prefix_byte, fill);
265 }
266 }
267 }
268
269 #[test]
270 fn test_encode_uncompressed_then_decode_matches_compressed() {
271 // Create an uncompressed key, encode it, decode it, and verify
272 // the decoded version matches the compressed form
273 let mut uncompressed = vec![0x04];
274 let x = [0x99u8; 32];
275 let mut y = [0xAA; 32];
276 y[31] = 0x01; // odd -> should get 0x03 prefix
277 uncompressed.extend_from_slice(&x);
278 uncompressed.extend_from_slice(&y);
279
280 let did_key = encode_p256_didkey(&uncompressed).unwrap();
281 let (decoded, _) = decode_didkey(&did_key).unwrap();
282
283 assert_eq!(decoded[0], 0x03); // odd y
284 assert_eq!(&decoded[1..], &x);
285 }
286
287 #[test]
288 fn test_k256_roundtrip() {
289 // Manually construct and decode a K-256 key
290 let mut key = vec![0x03];
291 key.extend_from_slice(&[0x77; 32]);
292
293 let mut payload = Vec::new();
294 payload.extend_from_slice(&K256_MULTICODEC);
295 payload.extend_from_slice(&key);
296
297 let encoded = bs58::encode(&payload).into_string();
298 let did_key = format!("did:key:z{}", encoded);
299
300 let (decoded, key_type) = decode_didkey(&did_key).unwrap();
301 assert_eq!(key_type, KeyType::K256);
302 assert_eq!(decoded, key);
303 }
304
305 #[test]
306 fn test_p256_and_k256_didkeys_differ() {
307 let key_bytes = vec![0x02; 33];
308
309 // Encode as P256
310 let p256_did = encode_p256_didkey(&key_bytes).unwrap();
311
312 // Encode as K256 manually
313 let mut k256_payload = Vec::new();
314 k256_payload.extend_from_slice(&K256_MULTICODEC);
315 k256_payload.extend_from_slice(&key_bytes);
316 let k256_did = format!("did:key:z{}", bs58::encode(&k256_payload).into_string());
317
318 assert_ne!(p256_did, k256_did);
319
320 // P256 starts with zDnae, K256 starts with zQ3s
321 assert!(p256_did.starts_with("did:key:zDnae"));
322 assert!(k256_did.starts_with("did:key:zQ3s"));
323 }
324
325 #[test]
326 fn test_unknown_multicodec_prefix() {
327 let mut payload = vec![0x01, 0x02]; // unknown prefix
328 payload.extend_from_slice(&[0x02; 33]);
329
330 let encoded = bs58::encode(&payload).into_string();
331 let result = decode_didkey(&format!("did:key:z{}", encoded));
332 assert!(result.is_err());
333 let err_msg = result.unwrap_err().to_string();
334 assert!(err_msg.contains("Unknown multicodec prefix"));
335 }
336}