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