1use crate::{
2 decode_raw, decode_seed, encode, encode_prefix, encode_seed, err, KeyPairType,
3 PREFIX_BYTE_CURVE, PREFIX_BYTE_PRIVATE,
4};
5
6use super::Result;
7use crypto_box::{
8 aead::{Aead, AeadCore},
9 Nonce, SalsaBox,
10};
11use ed25519::signature::digest::typenum::Unsigned;
12use std::fmt::{self, Debug};
13
14const XKEY_VERSION_V1: &[u8] = b"xkv1";
15
16use crypto_box::{PublicKey, SecretKey};
17use rand::{CryptoRng, Rng, RngCore};
18
19#[derive(Clone)]
22pub struct XKey {
23 public: PublicKey,
24 secret: Option<SecretKey>,
25}
26
27impl Debug for XKey {
28 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29 write!(f, "XKey")
30 }
31}
32
33impl XKey {
34 #[cfg(not(target_arch = "wasm32"))]
39 pub fn new() -> Self {
40 Self::new_with_rand(&mut rand::rngs::OsRng)
41 }
42
43 #[cfg(not(target_arch = "wasm32"))]
50 pub fn new_with_rand(rand: &mut (impl CryptoRng + RngCore)) -> Self {
51 Self::new_from_raw(rand.gen())
52 }
53
54 pub fn new_from_raw(random_bytes: [u8; 32]) -> Self {
58 let private = SecretKey::from_bytes(random_bytes);
59 Self {
60 public: private.public_key(),
61 secret: Some(private),
62 }
63 }
64
65 pub fn from_public_key(source: &str) -> Result<Self> {
67 let source_bytes = source.as_bytes();
68 let raw = decode_raw(source_bytes)?;
69
70 let (prefix, rest) = raw.split_first().ok_or(err!(VerifyError, "Empty key"))?;
71 if *prefix != PREFIX_BYTE_CURVE {
72 Err(err!(
73 InvalidPrefix,
74 "Not a valid public key prefix: {}",
75 raw[0]
76 ))
77 } else {
78 let public = PublicKey::try_from(rest)
79 .map_err(|_| err!(VerifyError, "Could not read public key"))?;
80
81 Ok(Self {
82 public,
83 secret: None,
84 })
85 }
86 }
87
88 pub fn from_seed(source: &str) -> Result<Self> {
90 let (ty, seed) = decode_seed(source)?;
91
92 if ty != PREFIX_BYTE_CURVE {
93 return Err(err!(
94 InvalidPrefix,
95 "Expected a curve, got {:?}",
96 KeyPairType::from(ty)
97 ));
98 }
99
100 let secret = SecretKey::from_bytes(seed);
101 Ok(Self {
102 public: secret.public_key(),
103 secret: Some(secret),
104 })
105 }
106
107 pub fn seed(&self) -> Result<String> {
111 let Some(secret) = &self.secret else {
112 return Err(err!(IncorrectKeyType, "This keypair has no seed"));
113 };
114
115 Ok(encode_seed(&KeyPairType::Curve, &secret.to_bytes()))
116 }
117
118 pub fn public_key(&self) -> String {
120 encode(&KeyPairType::Curve, self.public.as_bytes())
121 }
122
123 pub fn private_key(&self) -> Result<String> {
124 let Some(secret) = &self.secret else {
125 return Err(err!(IncorrectKeyType, "This keypair has no seed"));
126 };
127
128 Ok(encode_prefix(&[PREFIX_BYTE_PRIVATE], &secret.to_bytes()))
129 }
130
131 pub fn key_pair_type(&self) -> KeyPairType {
133 KeyPairType::Curve
134 }
135
136 pub fn open(&self, input: &[u8], sender: &Self) -> Result<Vec<u8>> {
137 let nonce_size = <SalsaBox as AeadCore>::NonceSize::to_usize();
138
139 let Some(secret_key) = &self.secret else {
140 return Err(err!(SignatureError, "Cannot open without a private key"));
141 };
142
143 if input.len() <= XKEY_VERSION_V1.len() + nonce_size {
144 return Err(err!(InvalidPayload, "Payload too short"));
145 }
146
147 let Some(input) = input.strip_prefix(XKEY_VERSION_V1) else {
148 return Err(err!(InvalidPrefix, "Cannot open message, wrong version"));
149 };
150
151 let (nonce, input) = input.split_at(nonce_size);
152
153 let b = SalsaBox::new(&sender.public, secret_key);
154 b.decrypt(nonce.into(), input)
155 .map_err(|_| err!(InvalidPayload, "Cannot decrypt payload"))
156 }
157
158 #[cfg(not(target_arch = "wasm32"))]
164 pub fn seal(&self, input: &[u8], recipient: &Self) -> Result<Vec<u8>> {
165 self.seal_with_rand(input, recipient, &mut rand::rngs::OsRng)
166 }
167
168 #[cfg(not(target_arch = "wasm32"))]
171 pub fn seal_with_rand(
172 &self,
173 input: &[u8],
174 recipient: &Self,
175 rand: impl CryptoRng + RngCore,
176 ) -> Result<Vec<u8>> {
177 let nonce = SalsaBox::generate_nonce(rand);
178 self.seal_with_nonce(input, recipient, nonce)
179 }
180
181 pub fn seal_with_nonce(&self, input: &[u8], recipient: &Self, nonce: Nonce) -> Result<Vec<u8>> {
184 let Some(private_key) = &self.secret else {
185 return Err(err!(SignatureError, "Cannot seal without a private key"));
186 };
187
188 let b = SalsaBox::new(&recipient.public, private_key);
189 let crypted = b
190 .encrypt(&nonce, input)
191 .map_err(|_| err!(SignatureError, "Cannot seal payload"))?; let mut out = Vec::with_capacity(
194 XKEY_VERSION_V1.len()
195 + <SalsaBox as AeadCore>::NonceSize::to_usize()
196 + input.len()
197 + <SalsaBox as AeadCore>::TagSize::to_usize(),
198 );
199 out.extend_from_slice(XKEY_VERSION_V1);
200 out.extend_from_slice(nonce.as_slice());
201 out.extend_from_slice(&crypted);
202
203 Ok(out)
204 }
205}
206
207#[cfg(not(target_arch = "wasm32"))]
208impl Default for XKey {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::error::ErrorKind;
218 const MESSAGE: &[u8] = b"this is super secret";
219
220 #[test]
221 fn seed_encode_decode_round_trip() {
222 let pair = XKey::new();
223 let s = pair.seed().unwrap();
224 let p = pair.public_key();
225
226 let pair2 = XKey::from_seed(s.as_str()).unwrap();
227 let s2 = pair2.seed().unwrap();
228
229 assert_eq!(s, s2);
230 assert_eq!(p, pair2.public_key());
231 }
232
233 #[test]
234 fn roundtrip_encoding_go_compat() {
235 let seed = "SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZM";
237 let pk = "XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA";
238
239 let pair = XKey::from_seed(seed).unwrap();
240
241 assert_eq!(pair.seed().unwrap(), seed);
242 assert_eq!(pair.public_key(), pk);
243 }
244
245 #[test]
246 fn from_seed_rejects_bad_prefix() {
247 let seed = "SZAIB67JMUPS5OKP6BZNCFTIMHOTS6JIX2C53TLSNEROIRFBJLSK3NUOVY";
248 let pair = XKey::from_seed(seed);
249 assert!(pair.is_err());
250 if let Err(e) = pair {
251 assert_eq!(e.kind(), ErrorKind::InvalidPrefix);
252 }
253 }
254
255 #[test]
256 fn from_seed_rejects_bad_length() {
257 let seed = "SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZMA";
258 let pair = XKey::from_seed(seed);
259 assert!(pair.is_err());
260 if let Err(e) = pair {
261 assert_eq!(e.kind(), ErrorKind::InvalidKeyLength);
262 }
263 }
264
265 #[test]
266 fn from_seed_rejects_invalid_encoding() {
267 let badseed = "SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS!GNHFXO5D4JJZM";
268 let pair = XKey::from_seed(badseed);
269 assert!(pair.is_err());
270 if let Err(e) = pair {
271 assert_eq!(e.kind(), ErrorKind::CodecFailure);
272 }
273 }
274
275 #[test]
276 fn public_key_round_trip() {
277 let src_pk = "XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA";
278 let account = XKey::from_public_key(src_pk).unwrap();
279 let pk = account.public_key();
280 assert_eq!(pk, src_pk);
281 }
282
283 #[test]
284 fn has_proper_prefix() {
285 let module = XKey::new();
286 assert!(module.seed().unwrap().starts_with("SX"));
287 assert!(module.public_key().starts_with('X'));
288 }
289
290 #[test]
291 fn xkeys_convert_to_public() {
292 let sender_pub =
293 XKey::from_public_key("XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA")
294 .unwrap();
295 let sender =
296 XKey::from_seed("SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZM").unwrap();
297
298 assert_eq!(sender.public_key(), sender_pub.public_key());
299 }
300
301 #[test]
302 fn seal_and_open() {
303 let sender = XKey::new();
304 let receiver = XKey::new();
305
306 let boxed = sender.seal(MESSAGE, &receiver).unwrap();
307
308 let res = receiver.open(&boxed, &sender).unwrap();
309 assert_eq!(MESSAGE, res.as_slice());
310 }
311
312 #[test]
313 fn tamper_version() {
314 let sender = XKey::new();
315 let receiver = XKey::new();
316
317 let mut boxed = sender.seal(MESSAGE, &receiver).unwrap();
318
319 boxed[0] += 1;
321
322 let err = receiver.open(&boxed, &sender).unwrap_err();
323 assert_eq!(err.kind(), ErrorKind::InvalidPrefix);
324 }
325
326 #[test]
327 fn tamper_message() {
328 let sender = XKey::new();
329 let receiver = XKey::new();
330
331 let mut boxed = sender.seal(MESSAGE, &receiver).unwrap();
332
333 boxed[XKEY_VERSION_V1.len() + 1] += 1;
335
336 let err = receiver.open(&boxed, &sender).unwrap_err();
337 assert_eq!(err.kind(), ErrorKind::InvalidPayload);
338 }
339
340 #[test]
341 fn wrong_key() {
342 let sender = XKey::new();
343 let receiver = XKey::new();
344 let random_key = XKey::new();
345
346 let boxed = sender.seal(MESSAGE, &receiver).unwrap();
347
348 let err = random_key.open(&boxed, &sender).unwrap_err();
349 assert_eq!(err.kind(), ErrorKind::InvalidPayload);
350 }
351
352 #[test]
353 fn open_from_go() {
354 let receiver =
355 XKey::from_seed("SXAHGC56LJFTSRXFC653AT7XZU6WGYIXU4XFPMCT62GGHFLUCSPVYP764M").unwrap();
356 let sender =
357 XKey::from_public_key("XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA")
358 .unwrap();
359 let raw_sender =
360 XKey::from_seed("SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZM").unwrap();
361 assert_eq!(sender.public_key(), raw_sender.public_key());
362
363 let boxed = [
365 0x78, 0x6b, 0x76, 0x31, 0x46, 0x76, 0x98, 0xf9, 0x87, 0x3, 0x50, 0x2f, 0x42, 0x41,
366 0xb7, 0xa7, 0x34, 0x72, 0x98, 0x0, 0x92, 0x9f, 0x6d, 0x9, 0x4b, 0x6, 0xc6, 0xe3, 0x4a,
367 0x78, 0xde, 0x49, 0x9e, 0xe7, 0xde, 0xbb, 0xac, 0x94, 0x77, 0x55, 0x6f, 0x3f, 0xbb,
368 0xe9, 0xf, 0xfd, 0x67, 0x8b, 0xc6, 0x29, 0xe5, 0xb7, 0xcc, 0x7c, 0x57, 0x40, 0x4d,
369 0x92, 0x38, 0x46, 0xcf, 0x1, 0x2, 0x26,
370 ];
371
372 let out = receiver.open(&boxed, &raw_sender).unwrap();
373 assert_eq!(std::str::from_utf8(&out), Ok("this is super secret"));
374 }
375}