nkeys/
xkeys.rs

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/// The main interface used for reading and writing _nkey-encoded_ curve key
20/// pairs.
21#[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    /// Creates a new xkey.
35    ///
36    /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of
37    /// rand support. Use [`new_from_raw`](XKey::new_from_raw) instead
38    #[cfg(not(target_arch = "wasm32"))]
39    pub fn new() -> Self {
40        Self::new_with_rand(&mut rand::rngs::OsRng)
41    }
42
43    /// Create a new xkey pair from a random generator
44    ///
45    /// NOTE: These generator should be a cryptographically secure random source.
46    ///
47    /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of
48    /// rand support. Use [`new_from_raw`](XKey::new_from_raw) instead
49    #[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    /// Create a new xkey pair using a pre-existing set of random bytes.
55    ///
56    /// NOTE: These bytes should be generated from a cryptographically secure random source.
57    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    /// Attempts to produce a public-only xkey from the given encoded public key string
66    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    /// Attempts to produce a full xkey pair from the given encoded seed string
89    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    /// Attempts to return the encoded, human-readable string for this key pair's seed.
108    /// Remember that this value should be treated as a secret. Do not store it for
109    /// any longer than necessary
110    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    /// Returns the encoded, human-readable public key of this key pair
119    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    /// Returns the type of this key pair.
132    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    /// Seal is compatible with nacl.Box.Seal() and can be used in similar situations for small
159    /// messages. We generate the nonce from crypto rand by default.
160    ///
161    /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of
162    /// rand support. Use [`seal_with_nonce`](XKey::seal_with_nonce) instead
163    #[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    /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of
169    /// rand support. Use [`seal_with_nonce`](XKey::seal_with_nonce) instead
170    #[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    /// NOTE: Nonce bytes should be generated from a cryptographically secure random source, and
182    /// only be used once.
183    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"))?; // Can't fail when used with SalsaBox
192
193        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        // Seed and Public Key pair generated by Go nkeys library
236        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        // Tamper with message
320        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        // Tamper with message
334        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        // Message generated with nkeys Go library
364        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}