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
at main 336 lines 11 kB view raw
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}