Skip to main content

mini_sign/
secret_key.rs

1use std::fmt::Display;
2
3use crate::{
4    public_key::{PublicKey, RawPk},
5    util::{raw_scrypt_params, validate_comment},
6    ErrorKind, PublicKeyBox, Result, SError, ALG_SIZE, CHK_ALG, CHK_SIZE, COMPONENT_SIZE, KDF_ALG,
7    KDF_ALG_NONE, KDF_LIMIT_SIZE, KDF_SALT_SIZE, KEYNUM_SK_SIZE, KEY_SIG_ALG, KID_SIZE, MEMLIMIT,
8    N_LOG2_MAX, OPSLIMIT,
9};
10use base64::Engine;
11use blake2::{Blake2b, Digest};
12use ed25519_dalek::{
13    ed25519::{self, ComponentBytes},
14    Signer,
15};
16use getrandom::rand_core::TryRng;
17use zeroize::{Zeroize, ZeroizeOnDrop};
18
19/// A `SecretKeyBox` represents a minisign secret key.
20///
21/// also can be output to a string and parse from a str.
22#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)]
23pub struct SecretKeyBox<'s> {
24    #[zeroize(skip)]
25    pub(crate) untrusted_comment: Option<&'s str>,
26    secret_key: SecretKey,
27}
28impl Display for SecretKeyBox<'_> {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        let mut s = String::new();
31        s.push_str("untrusted comment: ");
32        if let Some(c) = self.untrusted_comment {
33            s.push_str(c);
34        }
35        s.push('\n');
36        let encoder = base64::engine::general_purpose::STANDARD;
37        let mut sk_format = vec![];
38        sk_format.extend_from_slice(&self.secret_key.sig_alg);
39        sk_format.extend_from_slice(&self.secret_key.kdf_alg);
40        sk_format.extend_from_slice(&self.secret_key.cksum_alg);
41        sk_format.extend_from_slice(&self.secret_key.kdf_salt);
42        sk_format.extend_from_slice(&self.secret_key.kdf_opslimit.to_le_bytes());
43        sk_format.extend_from_slice(&self.secret_key.kdf_memlimit.to_le_bytes());
44        sk_format.extend_from_slice(&self.secret_key.keynum_sk);
45        let mut sk = encoder.encode(&sk_format);
46        sk_format.zeroize();
47        s.push_str(&sk);
48        sk.zeroize();
49        s.push('\n');
50
51        write!(f, "{}", s)
52    }
53}
54type Blake2b256 = Blake2b<blake2::digest::consts::U32>;
55impl<'s> SecretKeyBox<'s> {
56    fn new(untrusted_comment: Option<&'s str>, secret_key: SecretKey) -> Result<Self> {
57        validate_comment(untrusted_comment, ErrorKind::SecretKey)?;
58        Ok(Self {
59            untrusted_comment,
60            secret_key,
61        })
62    }
63    pub(crate) fn sig_alg(&self) -> [u8; ALG_SIZE] {
64        self.secret_key.sig_alg
65    }
66    pub fn from_signing_key(
67        signing_key: ed25519_dalek::SigningKey,
68        kid: &[u8; KID_SIZE],
69        password: Option<&[u8]>,
70        untrusted_comment: Option<&'s str>,
71    ) -> Result<Self> {
72        let sk = signing_key.to_bytes();
73        let pk = signing_key.verifying_key().to_bytes();
74        let (kdf_alg, kdf_salt, kdf_opslimit, kdf_memlimit, mut kdf_buf, checksum) =
75            if let Some(password) = password {
76                let mut kdf_salt = [0u8; KDF_SALT_SIZE];
77                getrandom::SysRng.try_fill_bytes(&mut kdf_salt)?;
78                let mut hash = Blake2b256::new();
79                hash.update(KEY_SIG_ALG);
80                hash.update(kid);
81                hash.update(sk);
82                hash.update(pk);
83                (
84                    KDF_ALG,
85                    kdf_salt,
86                    OPSLIMIT,
87                    MEMLIMIT,
88                    kdf(Some(password), &kdf_salt, OPSLIMIT, MEMLIMIT)?,
89                    hash.finalize().to_vec().try_into().unwrap(),
90                )
91            } else {
92                (
93                    KDF_ALG_NONE,
94                    [0u8; KDF_SALT_SIZE],
95                    0,
96                    0,
97                    [0u8; KEYNUM_SK_SIZE],
98                    [0u8; CHK_SIZE],
99                )
100            };
101        let keynum_sk = KeynumSK {
102            key_id: *kid,
103            sec_key: RawSk(sk),
104            pub_key: pk,
105            checksum,
106        };
107        kdf_buf = keynum_sk.to_bytes(kdf_buf);
108        let secret_key = SecretKey {
109            sig_alg: KEY_SIG_ALG,
110            kdf_alg,
111            cksum_alg: CHK_ALG,
112            kdf_salt,
113            kdf_opslimit,
114            kdf_memlimit,
115            keynum_sk: kdf_buf,
116        };
117        kdf_buf.zeroize();
118        Self::new(untrusted_comment, secret_key)
119    }
120    pub(crate) fn sign(
121        &self,
122        message: &[u8],
123        password: Option<&[u8]>,
124    ) -> Result<ed25519::Signature> {
125        self.secret_key.sign(message, password)
126    }
127    pub(crate) fn xor_keynum_sk(&self, password: Option<&[u8]>) -> Result<KeynumSK> {
128        self.secret_key.xor_keynum_sk(password)
129    }
130    /// Get the public key from the secret key, without untrusted comment.
131    /// only one line.
132    pub fn from_raw_str(s: &'s str) -> Result<Self> {
133        let secret_key = s.trim();
134        let decoder = base64::engine::general_purpose::STANDARD;
135        let mut sk_format = decoder
136            .decode(secret_key.as_bytes())
137            .map_err(|e| SError::new(crate::ErrorKind::SecretKey, e))?;
138        if sk_format.len()
139            != ALG_SIZE
140                + ALG_SIZE
141                + ALG_SIZE
142                + KDF_SALT_SIZE
143                + KDF_LIMIT_SIZE
144                + KDF_LIMIT_SIZE
145                + KEYNUM_SK_SIZE
146        {
147            return Err(SError::new(
148                crate::ErrorKind::SecretKey,
149                "invalid secret key length",
150            ));
151        }
152        let sig_alg = &sk_format[..ALG_SIZE];
153        let kdf_alg = &sk_format[ALG_SIZE..ALG_SIZE + ALG_SIZE];
154        let cksum_alg = &sk_format[ALG_SIZE + ALG_SIZE..ALG_SIZE + ALG_SIZE + ALG_SIZE];
155        let kdf_salt = &sk_format
156            [ALG_SIZE + ALG_SIZE + ALG_SIZE..ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE];
157        let kdf_opslimit = u64::from_le_bytes(
158            sk_format[ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE
159                ..ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE + KDF_LIMIT_SIZE]
160                .try_into()
161                .unwrap(),
162        );
163        let kdf_memlimit = u64::from_le_bytes(
164            sk_format[ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE + KDF_LIMIT_SIZE
165                ..ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE + KDF_LIMIT_SIZE + KDF_LIMIT_SIZE]
166                .try_into()
167                .unwrap(),
168        );
169
170        let secret_key = SecretKey {
171            sig_alg: sig_alg.try_into().unwrap(),
172            kdf_alg: kdf_alg.try_into().unwrap(),
173            cksum_alg: cksum_alg.try_into().unwrap(),
174            kdf_salt: kdf_salt.try_into().unwrap(),
175            kdf_opslimit,
176            kdf_memlimit,
177            keynum_sk: sk_format[ALG_SIZE
178                + ALG_SIZE
179                + ALG_SIZE
180                + KDF_SALT_SIZE
181                + KDF_LIMIT_SIZE
182                + KDF_LIMIT_SIZE..]
183                .try_into()
184                .unwrap(),
185        };
186        sk_format.zeroize();
187        SecretKeyBox::new(None, secret_key)
188    }
189    /// Parse a `SecretKeyBox` from str.
190    ///
191    /// as it store in a file.
192    #[allow(clippy::should_implement_trait)]
193    pub fn from_str(s: &'s str) -> Result<Self> {
194        parse_secret_key(s)
195    }
196    /// Get the untrusted comment.
197    pub fn untrusted_comment(&self) -> Option<&'s str> {
198        self.untrusted_comment
199    }
200    /// Get public key from the secret key.
201    pub fn public_key(&self, password: Option<&[u8]>) -> Result<PublicKeyBox<'s>> {
202        pub_key_from_sec_key(self, password)
203    }
204}
205fn pub_key_from_sec_key<'s>(
206    sec_key: &SecretKeyBox<'s>,
207    password: Option<&[u8]>,
208) -> Result<PublicKeyBox<'s>> {
209    let keynum_sk = sec_key.xor_keynum_sk(password)?;
210    let pk_box = PublicKeyBox::new(
211        None,
212        PublicKey::new(
213            sec_key.secret_key.sig_alg,
214            keynum_sk.key_id,
215            RawPk(keynum_sk.pub_key),
216        ),
217    );
218    pk_box
219}
220
221fn parse_raw_secret_key(secret_key: &str) -> Result<SecretKey> {
222    let decoder = base64::engine::general_purpose::STANDARD;
223    let mut sk_format = decoder
224        .decode(secret_key.as_bytes())
225        .map_err(|e| SError::new(crate::ErrorKind::SecretKey, e))?;
226    if sk_format.len()
227        != ALG_SIZE
228            + ALG_SIZE
229            + ALG_SIZE
230            + KDF_SALT_SIZE
231            + KDF_LIMIT_SIZE
232            + KDF_LIMIT_SIZE
233            + KEYNUM_SK_SIZE
234    {
235        return Err(SError::new(
236            crate::ErrorKind::SecretKey,
237            "invalid secret key length",
238        ));
239    }
240    let sig_alg = &sk_format[..ALG_SIZE];
241    let kdf_alg = &sk_format[ALG_SIZE..ALG_SIZE + ALG_SIZE];
242    let cksum_alg = &sk_format[ALG_SIZE + ALG_SIZE..ALG_SIZE + ALG_SIZE + ALG_SIZE];
243    let kdf_salt =
244        &sk_format[ALG_SIZE + ALG_SIZE + ALG_SIZE..ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE];
245    let kdf_opslimit = u64::from_le_bytes(
246        sk_format[ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE
247            ..ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE + KDF_LIMIT_SIZE]
248            .try_into()
249            .unwrap(),
250    );
251    let kdf_memlimit = u64::from_le_bytes(
252        sk_format[ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE + KDF_LIMIT_SIZE
253            ..ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE + KDF_LIMIT_SIZE + KDF_LIMIT_SIZE]
254            .try_into()
255            .unwrap(),
256    );
257
258    let secret_key = SecretKey {
259        sig_alg: sig_alg.try_into().unwrap(),
260        kdf_alg: kdf_alg.try_into().unwrap(),
261        cksum_alg: cksum_alg.try_into().unwrap(),
262        kdf_salt: kdf_salt.try_into().unwrap(),
263        kdf_opslimit,
264        kdf_memlimit,
265        keynum_sk: sk_format
266            [ALG_SIZE + ALG_SIZE + ALG_SIZE + KDF_SALT_SIZE + KDF_LIMIT_SIZE + KDF_LIMIT_SIZE..]
267            .try_into()
268            .unwrap(),
269    };
270    sk_format.zeroize();
271    Ok(secret_key)
272}
273fn parse_secret_key(s: &str) -> Result<SecretKeyBox<'_>> {
274    let mut lines = s.lines();
275    let untrusted_comment = lines
276        .next()
277        .ok_or_else(|| SError::new(crate::ErrorKind::SecretKey, "missing untrusted comment"))?
278        .strip_prefix("untrusted comment: ")
279        .ok_or_else(|| SError::new(crate::ErrorKind::SecretKey, "missing untrusted comment"))?;
280    validate_comment(Some(untrusted_comment), ErrorKind::SecretKey)?;
281    let secret_key = lines
282        .next()
283        .ok_or_else(|| SError::new(crate::ErrorKind::SecretKey, "missing secret key"))?;
284    if lines.next().is_some() {
285        return Err(SError::new(
286            crate::ErrorKind::SecretKey,
287            "unexpected extra data",
288        ));
289    }
290    SecretKeyBox::new(Some(untrusted_comment), parse_raw_secret_key(secret_key)?)
291}
292
293#[cfg(test)]
294#[test]
295fn test_parse_secret_key() {
296    use crate::KeyPairBox;
297    let password = b"password";
298    let k = KeyPairBox::generate(Some(password), None, None).unwrap();
299    let file = k.secret_key_box.to_string();
300    let sk = parse_secret_key(&file).unwrap();
301    assert_eq!(file, sk.to_string());
302}
303#[cfg(test)]
304#[test]
305fn test_parse_secret_key_rejects_extra_lines() {
306    use crate::KeyPairBox;
307
308    let keypair = KeyPairBox::generate(Some(b"password"), None, None).unwrap();
309    let file = format!("{}extra\n", keypair.secret_key_box);
310
311    assert!(parse_secret_key(&file).is_err());
312}
313#[cfg(test)]
314#[test]
315fn test_parse_secret_key_requires_comment_prefix() {
316    use crate::KeyPairBox;
317
318    let keypair = KeyPairBox::generate(Some(b"password"), None, None).unwrap();
319    let secret_key = keypair
320        .secret_key_box
321        .to_string()
322        .lines()
323        .nth(1)
324        .unwrap()
325        .to_owned();
326    let malformed = format!("bad comment\n{secret_key}\n");
327
328    assert!(parse_secret_key(&malformed).is_err());
329}
330#[cfg(test)]
331#[test]
332fn test_generate_rejects_comment_control_characters() {
333    use crate::KeyPairBox;
334
335    assert!(KeyPairBox::generate(Some(b"password"), Some("bad\ncomment"), None).is_err());
336    assert!(KeyPairBox::generate(Some(b"password"), None, Some("bad\0comment")).is_err());
337}
338/// A `SecretKey` is used to sign messages.
339#[derive(Clone, Debug, ZeroizeOnDrop, PartialEq, Eq)]
340pub(crate) struct SecretKey {
341    pub(crate) sig_alg: [u8; ALG_SIZE],
342    kdf_alg: [u8; ALG_SIZE],
343    cksum_alg: [u8; ALG_SIZE],
344    kdf_salt: [u8; KDF_SALT_SIZE],
345    kdf_opslimit: u64,
346    kdf_memlimit: u64,
347    keynum_sk: [u8; KEYNUM_SK_SIZE],
348}
349#[derive(Clone, Debug, ZeroizeOnDrop)]
350pub(crate) struct KeynumSK {
351    pub(crate) key_id: [u8; KID_SIZE],
352    sec_key: RawSk,
353    pub(crate) pub_key: ComponentBytes,
354    checksum: [u8; CHK_SIZE],
355}
356impl KeynumSK {
357    fn to_bytes(&self, mut kdf_buf: [u8; KEYNUM_SK_SIZE]) -> [u8; KEYNUM_SK_SIZE] {
358        for (i, item) in kdf_buf.iter_mut().enumerate().take(KID_SIZE) {
359            *item ^= self.key_id[i];
360        }
361        for i in 0..COMPONENT_SIZE {
362            kdf_buf[KID_SIZE + i] ^= self.sec_key.0[i];
363        }
364        for i in 0..COMPONENT_SIZE {
365            kdf_buf[KID_SIZE + COMPONENT_SIZE + i] ^= self.pub_key[i];
366        }
367        for i in 0..CHK_SIZE {
368            kdf_buf[KID_SIZE + 2 * COMPONENT_SIZE + i] ^= self.checksum[i];
369        }
370        kdf_buf
371    }
372    fn from_bytes(keynum_sk: &[u8; KEYNUM_SK_SIZE], mut kdf_buf: [u8; KEYNUM_SK_SIZE]) -> Self {
373        for i in 0..KEYNUM_SK_SIZE {
374            kdf_buf[i] ^= keynum_sk[i];
375        }
376        Self {
377            key_id: kdf_buf[0..KID_SIZE].try_into().unwrap(),
378            sec_key: RawSk(
379                kdf_buf[KID_SIZE..KID_SIZE + COMPONENT_SIZE]
380                    .try_into()
381                    .unwrap(),
382            ),
383            pub_key: kdf_buf[KID_SIZE + COMPONENT_SIZE..KID_SIZE + 2 * COMPONENT_SIZE]
384                .try_into()
385                .unwrap(),
386            checksum: kdf_buf[KID_SIZE + 2 * COMPONENT_SIZE..KEYNUM_SK_SIZE]
387                .try_into()
388                .unwrap(),
389        }
390    }
391}
392#[derive(Debug, Clone, ZeroizeOnDrop, Zeroize)]
393struct RawSk(ComponentBytes);
394impl Signer<ed25519::Signature> for RawSk {
395    fn try_sign(&self, msg: &[u8]) -> std::result::Result<ed25519::Signature, ed25519::Error> {
396        let sk = ed25519_dalek::SigningKey::from_bytes(&self.0);
397        Ok(sk.sign(msg))
398    }
399}
400fn kdf(
401    password: Option<&[u8]>,
402    salt: &[u8; KDF_SALT_SIZE],
403    opslimit: u64,
404    memlimit: u64,
405) -> Result<[u8; KEYNUM_SK_SIZE]> {
406    let params = raw_scrypt_params(memlimit as usize, opslimit, N_LOG2_MAX)?;
407    let mut stream = [0u8; KEYNUM_SK_SIZE];
408    scrypt::scrypt(password.unwrap_or(&[]), salt, &params, &mut stream)?;
409    Ok(stream)
410}
411impl SecretKey {
412    pub fn sign(&self, message: &[u8], password: Option<&[u8]>) -> Result<ed25519::Signature> {
413        let keynum_sk = self.xor_keynum_sk(password);
414        Ok(keynum_sk?.sec_key.sign(message))
415    }
416    pub(crate) fn xor_keynum_sk(&self, password: Option<&[u8]>) -> Result<KeynumSK> {
417        if self.sig_alg != KEY_SIG_ALG {
418            return Err(SError::new(
419                crate::ErrorKind::SecretKey,
420                "invalid secret key signature algorithm",
421            ));
422        }
423        if self.cksum_alg != CHK_ALG {
424            return Err(SError::new(
425                crate::ErrorKind::SecretKey,
426                "invalid secret key checksum algorithm",
427            ));
428        }
429        let stream = if self.kdf_alg == KDF_ALG {
430            kdf(
431                password,
432                &self.kdf_salt,
433                self.kdf_opslimit,
434                self.kdf_memlimit,
435            )?
436        } else if self.kdf_alg == KDF_ALG_NONE {
437            if password.is_some_and(|password| !password.is_empty()) {
438                return Err(SError::new(
439                    crate::ErrorKind::SecretKey,
440                    "secret key is not encrypted",
441                ));
442            }
443            [0u8; KEYNUM_SK_SIZE]
444        } else {
445            return Err(SError::new(
446                crate::ErrorKind::SecretKey,
447                "invalid secret key kdf algorithm",
448            ));
449        };
450
451        let keynum_sk = KeynumSK::from_bytes(&self.keynum_sk, stream);
452
453        if self.kdf_alg == KDF_ALG_NONE {
454            if keynum_sk.checksum != [0u8; CHK_SIZE] {
455                return Err(SError::new(
456                    crate::ErrorKind::SecretKey,
457                    "invalid unencrypted secret key checksum",
458                ));
459            }
460            let public_key = ed25519_dalek::SigningKey::from_bytes(&keynum_sk.sec_key.0)
461                .verifying_key()
462                .to_bytes();
463            if public_key != keynum_sk.pub_key {
464                return Err(SError::new(
465                    crate::ErrorKind::SecretKey,
466                    "secret key public key mismatch",
467                ));
468            }
469            return Ok(keynum_sk);
470        }
471
472        let mut hash = Blake2b256::new();
473        hash.update(self.sig_alg);
474        hash.update(&keynum_sk.key_id);
475        hash.update(&keynum_sk.sec_key.0);
476        hash.update(&keynum_sk.pub_key);
477        if hash.finalize().to_vec() != keynum_sk.checksum {
478            return Err(SError::new(
479                crate::ErrorKind::SecretKey,
480                "checksum mismatch, invalid password",
481            ));
482        }
483        Ok(keynum_sk)
484    }
485}
486#[cfg(test)]
487#[test]
488fn test_sign() {
489    use crate::{pub_key_from_sec_key, KeyPairBox};
490    let password = b"password";
491    let k = KeyPairBox::generate(Some(password), None, None).unwrap();
492    let s = k.secret_key_box.to_string();
493    let sk = parse_secret_key(&s).unwrap();
494    let msg = b"hello world";
495    let sig = sk.sign(msg, Some(password)).unwrap();
496    let pk = pub_key_from_sec_key(&sk, Some(password)).unwrap();
497    let v = pk.public_key.key.verify(msg, &sig);
498    assert!(v.unwrap());
499}