ssh_key/
private.rs

1//! SSH private key support.
2//!
3//! Support for decoding SSH private keys (i.e. digital signature keys)
4//! from the OpenSSH file format:
5//!
6//! <https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD>
7//!
8//! ## Decrypting encrypted private keys
9//!
10//! When the `encryption` feature of this crate is enabled, it's possible to
11//! decrypt keys which have been encrypted under a password:
12//!
13#![cfg_attr(all(feature = "encryption", feature = "std"), doc = " ```")]
14#![cfg_attr(not(all(feature = "encryption", feature = "std")), doc = " ```ignore")]
15//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! use ssh_key::PrivateKey;
17//!
18//! // WARNING: don't actually hardcode private keys in source code!!!
19//! let encoded_key = r#"
20//! -----BEGIN OPENSSH PRIVATE KEY-----
21//! b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBKH96ujW
22//! umB6/WnTNPjTeaAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN
23//! 796jTiQfZfG1KaT0PtFDJ/XFSqtiAAAAoFzvbvyFMhAiwBOXF0mhUUacPUCMZXivG2up2c
24//! hEnAw1b6BLRPyWbY5cC2n9ggD4ivJ1zSts6sBgjyiXQAReyrP35myYvT/OIB/NpwZM/xIJ
25//! N7MHSUzlkX4adBrga3f7GS4uv4ChOoxC4XsE5HsxtGsq1X8jzqLlZTmOcxkcEneYQexrUc
26//! bQP0o+gL5aKK8cQgiIlXeDbRjqhc4+h4EF6lY=
27//! -----END OPENSSH PRIVATE KEY-----
28//! "#;
29//!
30//! let encrypted_key = PrivateKey::from_openssh(encoded_key)?;
31//! assert!(encrypted_key.is_encrypted());
32//!
33//! // WARNING: don't hardcode passwords, and this one's bad anyway
34//! let password = "hunter42";
35//!
36//! let decrypted_key = encrypted_key.decrypt(password)?;
37//! assert!(!decrypted_key.is_encrypted());
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ## Encrypting plaintext private keys
43//!
44//! When the `encryption` feature of this crate is enabled, it's possible to
45//! encrypt plaintext private keys under a provided password.
46//!
47//! The example below also requires enabling this crate's `getrandom` feature.
48//!
49#![cfg_attr(
50    all(
51        feature = "ed25519",
52        feature = "encryption",
53        feature = "getrandom",
54        feature = "std"
55    ),
56    doc = " ```"
57)]
58#![cfg_attr(
59    not(all(
60        feature = "ed25519",
61        feature = "encryption",
62        feature = "getrandom",
63        feature = "std"
64    )),
65    doc = " ```ignore"
66)]
67//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
68//! use ssh_key::{Algorithm, PrivateKey, rand_core::{OsRng, TryRngCore}};
69//!
70//! // Generate a random key
71//! let unencrypted_key = PrivateKey::random(&mut OsRng.unwrap_err(), Algorithm::Ed25519)?;
72//!
73//! // WARNING: don't hardcode passwords, and this one's bad anyway
74//! let password = "hunter42";
75//!
76//! let encrypted_key = unencrypted_key.encrypt(&mut OsRng, password)?;
77//! assert!(encrypted_key.is_encrypted());
78//! # Ok(())
79//! # }
80//! ```
81//!
82//! ## Generating random keys
83//!
84//! This crate supports generation of random keys using algorithm-specific
85//! backends gated on cargo features.
86//!
87//! The examples below require enabling this crate's `getrandom` feature as
88//! well as the crate feature identified in backticks in the title of each
89//! example.
90//!
91#![cfg_attr(
92    all(feature = "ed25519", feature = "getrandom", feature = "std"),
93    doc = " ```"
94)]
95#![cfg_attr(
96    not(all(feature = "ed25519", feature = "getrandom", feature = "std")),
97    doc = " ```ignore"
98)]
99//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
100//! use ssh_key::{Algorithm, PrivateKey, rand_core::{OsRng, TryRngCore}};
101//!
102//! let private_key = PrivateKey::random(&mut OsRng.unwrap_err(), Algorithm::Ed25519)?;
103//! # Ok(())
104//! # }
105//! ```
106
107#[cfg(feature = "alloc")]
108mod dsa;
109#[cfg(feature = "ecdsa")]
110mod ecdsa;
111mod ed25519;
112mod keypair;
113#[cfg(feature = "alloc")]
114mod opaque;
115#[cfg(feature = "alloc")]
116mod rsa;
117#[cfg(feature = "alloc")]
118mod sk;
119
120pub use self::{
121    ed25519::{Ed25519Keypair, Ed25519PrivateKey},
122    keypair::KeypairData,
123};
124
125#[cfg(feature = "alloc")]
126pub use crate::{
127    Comment, SshSig,
128    private::{
129        dsa::{DsaKeypair, DsaPrivateKey},
130        opaque::{OpaqueKeypair, OpaqueKeypairBytes, OpaquePrivateKeyBytes},
131        rsa::{RsaKeypair, RsaPrivateKey},
132        sk::SkEd25519,
133    },
134    sha2::Digest,
135};
136
137#[cfg(feature = "ecdsa")]
138pub use self::ecdsa::{EcdsaKeypair, EcdsaPrivateKey};
139
140#[cfg(all(feature = "alloc", feature = "ecdsa"))]
141pub use self::sk::SkEcdsaSha2NistP256;
142
143use crate::{Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result, public};
144use cipher::Tag;
145use core::str;
146use encoding::{
147    CheckedSum, Decode, DecodePem, Encode, EncodePem, Reader, Writer,
148    pem::{LineEnding, PemLabel},
149};
150use subtle::{Choice, ConstantTimeEq};
151
152#[cfg(feature = "alloc")]
153use {
154    crate::AssociatedHashAlg,
155    alloc::{string::String, vec::Vec},
156    zeroize::Zeroizing,
157};
158
159#[cfg(feature = "encryption")]
160use rand_core::TryCryptoRng;
161
162#[cfg(feature = "rand_core")]
163use rand_core::CryptoRng;
164
165#[cfg(feature = "std")]
166use std::{fs::File, path::Path};
167
168#[cfg(feature = "std")]
169use std::io::{self, Read, Write};
170
171#[cfg(all(unix, feature = "std"))]
172use std::os::unix::fs::OpenOptionsExt;
173
174/// Error message for infallible conversions (used by `expect`)
175const CONVERSION_ERROR_MSG: &str = "SSH private key conversion error";
176
177/// Default key size to use for RSA keys in bits.
178#[cfg(all(feature = "rand_core", feature = "rsa"))]
179const DEFAULT_RSA_KEY_SIZE: usize = 4096;
180
181/// Maximum supported block size.
182///
183/// This is the block size used by e.g. AES.
184const MAX_BLOCK_SIZE: usize = 16;
185
186/// Padding bytes to use.
187const PADDING_BYTES: [u8; MAX_BLOCK_SIZE] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
188
189/// Unix file permissions for SSH private keys.
190#[cfg(all(unix, feature = "std"))]
191const UNIX_FILE_PERMISSIONS: u32 = 0o600;
192
193/// SSH private key.
194#[derive(Clone, Debug)]
195pub struct PrivateKey {
196    /// Cipher algorithm.
197    cipher: Cipher,
198
199    /// KDF options.
200    kdf: Kdf,
201
202    /// "Checkint" value used to verify successful decryption.
203    checkint: Option<u32>,
204
205    /// Public key.
206    public_key: PublicKey,
207
208    /// Private keypair data.
209    key_data: KeypairData,
210
211    /// Authentication tag for authenticated encryption modes.
212    auth_tag: Option<Tag>,
213}
214
215impl PrivateKey {
216    /// Magic string used to identify keys in this format.
217    const AUTH_MAGIC: &'static [u8] = b"openssh-key-v1\0";
218
219    /// Create a new unencrypted private key with the given keypair data and comment.
220    ///
221    /// On `no_std` platforms, use `PrivateKey::from(key_data)` instead.
222    #[cfg(feature = "alloc")]
223    pub fn new(key_data: KeypairData, comment: impl Into<Comment>) -> Result<Self> {
224        if key_data.is_encrypted() {
225            return Err(Error::Encrypted);
226        }
227
228        let mut private_key = Self::try_from(key_data)?;
229        private_key.public_key.comment = comment.into();
230        Ok(private_key)
231    }
232
233    /// Parse an OpenSSH-formatted PEM private key.
234    ///
235    /// OpenSSH-formatted private keys begin with the following:
236    ///
237    /// ```text
238    /// -----BEGIN OPENSSH PRIVATE KEY-----
239    /// ```
240    pub fn from_openssh(pem: impl AsRef<[u8]>) -> Result<Self> {
241        Self::decode_pem(pem)
242    }
243
244    /// Parse a PuTTY PPK private key.
245    ///
246    /// PPK-formatted private keys begin with the following:
247    ///
248    /// ```text
249    /// PuTTY-User-Key-File-<VERSION>: <ALGORITHM>
250    /// ```
251    #[cfg(feature = "ppk")]
252    pub fn from_ppk(ppk: impl AsRef<str>, passphrase: Option<String>) -> Result<Self> {
253        use crate::ppk::PpkContainer;
254
255        let ppk: PpkContainer = PpkContainer::new(ppk.as_ref().try_into()?, passphrase)?;
256
257        Ok(Self {
258            auth_tag: None,
259            checkint: None,
260            cipher: Cipher::None,
261            kdf: Kdf::None,
262            key_data: ppk.keypair_data,
263            public_key: ppk.public_key,
264        })
265    }
266
267    /// Parse a raw binary SSH private key.
268    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
269        let reader = &mut bytes;
270        let private_key = Self::decode(reader)?;
271        Ok(reader.finish(private_key)?)
272    }
273
274    /// Encode OpenSSH-formatted (PEM) private key.
275    pub fn encode_openssh<'o>(
276        &self,
277        line_ending: LineEnding,
278        out: &'o mut [u8],
279    ) -> Result<&'o str> {
280        Ok(self.encode_pem(line_ending, out)?)
281    }
282
283    /// Encode an OpenSSH-formatted PEM private key, allocating a
284    /// self-zeroizing [`String`] for the result.
285    #[cfg(feature = "alloc")]
286    pub fn to_openssh(&self, line_ending: LineEnding) -> Result<Zeroizing<String>> {
287        Ok(self.encode_pem_string(line_ending).map(Zeroizing::new)?)
288    }
289
290    /// Serialize SSH private key as raw bytes.
291    #[cfg(feature = "alloc")]
292    pub fn to_bytes(&self) -> Result<Zeroizing<Vec<u8>>> {
293        let mut private_key_bytes = Vec::with_capacity(self.encoded_len()?);
294        self.encode(&mut private_key_bytes)?;
295        Ok(Zeroizing::new(private_key_bytes))
296    }
297
298    /// Sign the given message using this private key, returning an [`SshSig`].
299    ///
300    /// These signatures can be produced using `ssh-keygen -Y sign`. They're
301    /// encoded as PEM and begin with the following:
302    ///
303    /// ```text
304    /// -----BEGIN SSH SIGNATURE-----
305    /// ```
306    ///
307    /// See [PROTOCOL.sshsig] for more information.
308    ///
309    /// # Usage
310    ///
311    /// See also: [`PublicKey::verify`].
312    ///
313    #[cfg_attr(feature = "ed25519", doc = "```")]
314    #[cfg_attr(not(feature = "ed25519"), doc = "```ignore")]
315    /// # fn main() -> Result<(), ssh_key::Error> {
316    /// use ssh_key::{PrivateKey, HashAlg, SshSig};
317    ///
318    /// // Message to be signed.
319    /// let message = b"testing";
320    ///
321    /// // Example domain/namespace used for the message.
322    /// let namespace = "example";
323    ///
324    /// // Private key to use when computing the signature.
325    /// // WARNING: don't actually hardcode private keys in source code!!!
326    /// let encoded_private_key = r#"
327    /// -----BEGIN OPENSSH PRIVATE KEY-----
328    /// b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
329    /// QyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYgAAAJgAIAxdACAM
330    /// XQAAAAtzc2gtZWQyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYg
331    /// AAAEC2BsIi0QwW2uFscKTUUXNHLsYX4FxlaSDSblbAj7WR7bM+rvN+ot98qgEN796jTiQf
332    /// ZfG1KaT0PtFDJ/XFSqtiAAAAEHVzZXJAZXhhbXBsZS5jb20BAgMEBQ==
333    /// -----END OPENSSH PRIVATE KEY-----
334    /// "#;
335    ///
336    /// let private_key = encoded_private_key.parse::<PrivateKey>()?;
337    /// let signature = private_key.sign(namespace, HashAlg::default(), message)?;
338    /// // assert!(private_key.public_key().verify(namespace, message, &signature).is_ok());
339    /// # Ok(())
340    /// # }
341    /// ```
342    ///
343    /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
344    #[cfg(feature = "alloc")]
345    pub fn sign(&self, namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result<SshSig> {
346        SshSig::sign(self, namespace, hash_alg, msg)
347    }
348
349    /// Sign the given message [`Digest`] using this private key, returning an [`SshSig`].
350    ///
351    /// These signatures can be produced using `ssh-keygen -Y sign`.
352    ///
353    /// For more information, see [`PrivateKey::sign`].
354    #[cfg(feature = "alloc")]
355    pub fn sign_digest<D: AssociatedHashAlg + Digest>(
356        &self,
357        namespace: &str,
358        digest: D,
359    ) -> Result<SshSig> {
360        SshSig::sign_digest(self, namespace, digest)
361    }
362
363    /// Sign the given raw message prehash using this private key, returning an [`SshSig`].
364    ///
365    /// These signatures can be produced using `ssh-keygen -Y sign`.
366    ///
367    /// For more information, see [`PrivateKey::sign`].
368    #[cfg(feature = "alloc")]
369    pub fn sign_prehash(
370        &self,
371        namespace: &str,
372        hash_alg: HashAlg,
373        prehash: &[u8],
374    ) -> Result<SshSig> {
375        SshSig::sign_prehash(self, namespace, hash_alg, prehash)
376    }
377
378    /// Read private key from an OpenSSH-formatted PEM source.
379    #[cfg(feature = "std")]
380    pub fn read_openssh(reader: &mut impl Read) -> Result<Self> {
381        let pem = Zeroizing::new(io::read_to_string(reader)?);
382        Self::from_openssh(&*pem)
383    }
384
385    /// Read private key from an OpenSSH-formatted PEM file.
386    #[cfg(feature = "std")]
387    pub fn read_openssh_file(path: impl AsRef<Path>) -> Result<Self> {
388        // TODO(tarcieri): verify file permissions match `UNIX_FILE_PERMISSIONS`
389        let mut file = File::open(path)?;
390        Self::read_openssh(&mut file)
391    }
392
393    /// Write private key as an OpenSSH-formatted PEM file.
394    #[cfg(feature = "std")]
395    pub fn write_openssh(&self, writer: &mut impl Write, line_ending: LineEnding) -> Result<()> {
396        let pem = self.to_openssh(line_ending)?;
397        writer.write_all(pem.as_bytes())?;
398        Ok(())
399    }
400
401    /// Write private key as an OpenSSH-formatted PEM file.
402    #[cfg(feature = "std")]
403    pub fn write_openssh_file(
404        &self,
405        path: impl AsRef<Path>,
406        line_ending: LineEnding,
407    ) -> Result<()> {
408        let mut options = File::options();
409
410        #[cfg(unix)]
411        options.mode(UNIX_FILE_PERMISSIONS);
412
413        let mut file = options.write(true).create(true).truncate(true).open(path)?;
414
415        self.write_openssh(&mut file, line_ending)
416    }
417
418    /// Attempt to decrypt an encrypted private key using the provided
419    /// password to derive an encryption key.
420    ///
421    /// Returns [`Error::Decrypted`] if the private key is already decrypted.
422    #[cfg(feature = "encryption")]
423    pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result<Self> {
424        let (key, iv) = self.kdf.derive_key_and_iv(self.cipher, password)?;
425
426        let ciphertext = self.key_data.encrypted().ok_or(Error::Decrypted)?;
427        let mut buffer = Zeroizing::new(ciphertext.to_vec());
428        self.cipher.decrypt(&key, &iv, &mut buffer, self.auth_tag)?;
429
430        #[allow(clippy::arithmetic_side_effects)] // block sizes are constants
431        Self::decode_privatekey_comment_pair(
432            &mut &**buffer,
433            self.public_key.key_data.clone(),
434            self.cipher.block_size(),
435            self.cipher.block_size() - 1,
436        )
437    }
438
439    /// Encrypt an unencrypted private key using the provided password to
440    /// derive an encryption key.
441    ///
442    /// Uses the following algorithms:
443    /// - Cipher: [`Cipher::Aes256Ctr`]
444    /// - KDF: [`Kdf::Bcrypt`] (i.e. `bcrypt-pbkdf`)
445    ///
446    /// Returns [`Error::Encrypted`] if the private key is already encrypted.
447    #[cfg(feature = "encryption")]
448    pub fn encrypt<R: TryCryptoRng + ?Sized>(
449        &self,
450        rng: &mut R,
451        password: impl AsRef<[u8]>,
452    ) -> Result<Self> {
453        self.encrypt_with_cipher(rng, Cipher::Aes256Ctr, password)
454    }
455
456    /// Encrypt an unencrypted private key using the provided password to
457    /// derive an encryption key for the provided [`Cipher`].
458    ///
459    /// Returns [`Error::Encrypted`] if the private key is already encrypted.
460    #[cfg(feature = "encryption")]
461    pub fn encrypt_with_cipher<R: TryCryptoRng + ?Sized>(
462        &self,
463        rng: &mut R,
464        cipher: Cipher,
465        password: impl AsRef<[u8]>,
466    ) -> Result<Self> {
467        let checkint = rng.try_next_u32().map_err(|_| Error::RngFailure)?;
468
469        self.encrypt_with(
470            cipher,
471            Kdf::new(Default::default(), rng)?,
472            checkint,
473            password,
474        )
475    }
476
477    /// Encrypt an unencrypted private key using the provided cipher and KDF
478    /// configuration.
479    ///
480    /// Returns [`Error::Encrypted`] if the private key is already encrypted.
481    #[cfg(feature = "encryption")]
482    pub fn encrypt_with(
483        &self,
484        cipher: Cipher,
485        kdf: Kdf,
486        checkint: u32,
487        password: impl AsRef<[u8]>,
488    ) -> Result<Self> {
489        if self.is_encrypted() {
490            return Err(Error::Encrypted);
491        }
492
493        let (key_bytes, iv_bytes) = kdf.derive_key_and_iv(cipher, password)?;
494        let msg_len = self.encoded_privatekey_comment_pair_len(cipher)?;
495        let mut out = Vec::with_capacity(msg_len);
496
497        // Encode and encrypt private key
498        self.encode_privatekey_comment_pair(&mut out, cipher, checkint)?;
499        let auth_tag = cipher.encrypt(&key_bytes, &iv_bytes, out.as_mut_slice())?;
500
501        Ok(Self {
502            cipher,
503            kdf,
504            checkint: None,
505            public_key: self.public_key.key_data.clone().into(),
506            key_data: KeypairData::Encrypted(out),
507            auth_tag,
508        })
509    }
510
511    /// Get the digital signature [`Algorithm`] used by this key.
512    pub fn algorithm(&self) -> Algorithm {
513        self.public_key.algorithm()
514    }
515
516    /// Comment on the key (e.g. email address).
517    #[cfg(feature = "alloc")]
518    pub fn comment(&self) -> &Comment {
519        self.public_key.comment()
520    }
521
522    /// Cipher algorithm (a.k.a. `ciphername`).
523    pub fn cipher(&self) -> Cipher {
524        self.cipher
525    }
526
527    /// Compute key fingerprint.
528    ///
529    /// Use [`Default::default()`] to use the default hash function (SHA-256).
530    pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint {
531        self.public_key.fingerprint(hash_alg)
532    }
533
534    /// Is this key encrypted?
535    pub fn is_encrypted(&self) -> bool {
536        let ret = self.key_data.is_encrypted();
537        debug_assert_eq!(ret, self.cipher.is_some());
538        ret
539    }
540
541    /// Key Derivation Function (KDF) used to encrypt this key.
542    ///
543    /// Returns [`Kdf::None`] if this key is not encrypted.
544    pub fn kdf(&self) -> &Kdf {
545        &self.kdf
546    }
547
548    /// Keypair data.
549    pub fn key_data(&self) -> &KeypairData {
550        &self.key_data
551    }
552
553    /// Get the [`PublicKey`] which corresponds to this private key.
554    pub fn public_key(&self) -> &PublicKey {
555        &self.public_key
556    }
557
558    /// Generate a random key which uses the given algorithm.
559    ///
560    /// # Returns
561    /// - `Error::AlgorithmUnknown` if the algorithm is unsupported.
562    #[cfg(feature = "rand_core")]
563    #[allow(unreachable_code, unused_variables)]
564    pub fn random<R: CryptoRng + ?Sized>(rng: &mut R, algorithm: Algorithm) -> Result<Self> {
565        let checkint = rng.next_u32();
566
567        let key_data = match algorithm {
568            #[cfg(feature = "dsa")]
569            Algorithm::Dsa => KeypairData::from(DsaKeypair::random(rng)?),
570            #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))]
571            Algorithm::Ecdsa { curve } => KeypairData::from(EcdsaKeypair::random(rng, curve)?),
572            #[cfg(feature = "ed25519")]
573            Algorithm::Ed25519 => KeypairData::from(Ed25519Keypair::random(rng)),
574            #[cfg(feature = "rsa")]
575            Algorithm::Rsa { .. } => {
576                KeypairData::from(RsaKeypair::random(rng, DEFAULT_RSA_KEY_SIZE)?)
577            }
578            _ => return Err(Error::AlgorithmUnknown),
579        };
580        let public_key = public::KeyData::try_from(&key_data)?;
581
582        Ok(Self {
583            cipher: Cipher::None,
584            kdf: Kdf::None,
585            checkint: Some(checkint),
586            public_key: public_key.into(),
587            key_data,
588            auth_tag: None,
589        })
590    }
591
592    /// Set the comment on the key.
593    #[cfg(feature = "alloc")]
594    pub fn set_comment(&mut self, comment: impl Into<Comment>) {
595        self.public_key.set_comment(comment);
596    }
597
598    /// Decode [`KeypairData`] along with its associated checkints and comment,
599    /// storing the comment in the provided public key on success.
600    ///
601    /// This method also checks padding for validity and ensures that the
602    /// decoded private key matches the provided public key.
603    ///
604    /// For private key format specification, see OpenSSH [PROTOCOL.key] ยง 3:
605    ///
606    /// ```text
607    /// uint32  checkint
608    /// uint32  checkint
609    /// byte[]  privatekey1
610    /// string  comment1
611    /// byte[]  privatekey2
612    /// string  comment2
613    /// ...
614    /// string  privatekeyN
615    /// string  commentN
616    /// char    1
617    /// char    2
618    /// char    3
619    /// ...
620    /// char    padlen % 255
621    /// ```
622    ///
623    /// [PROTOCOL.key]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
624    fn decode_privatekey_comment_pair(
625        reader: &mut impl Reader,
626        public_key: public::KeyData,
627        block_size: usize,
628        max_padding_size: usize,
629    ) -> Result<Self> {
630        debug_assert!(block_size <= MAX_BLOCK_SIZE);
631        debug_assert!(max_padding_size <= MAX_BLOCK_SIZE);
632
633        // Ensure input data is padding-aligned
634        if reader.remaining_len().checked_rem(block_size) != Some(0) {
635            return Err(encoding::Error::Length.into());
636        }
637
638        let checkint1 = u32::decode(reader)?;
639        let checkint2 = u32::decode(reader)?;
640
641        if checkint1 != checkint2 {
642            return Err(Error::Crypto);
643        }
644
645        let key_data = KeypairData::decode(reader)?;
646
647        // Ensure public key matches private key
648        if public_key != public::KeyData::try_from(&key_data)? {
649            return Err(Error::PublicKey);
650        }
651
652        let mut public_key = PublicKey::from(public_key);
653        public_key.decode_comment(reader)?;
654
655        let padding_len = reader.remaining_len();
656
657        if padding_len > max_padding_size {
658            return Err(encoding::Error::Length.into());
659        }
660
661        if padding_len != 0 {
662            let mut padding = [0u8; MAX_BLOCK_SIZE];
663            reader.read(&mut padding[..padding_len])?;
664
665            if PADDING_BYTES[..padding_len] != padding[..padding_len] {
666                return Err(Error::FormatEncoding);
667            }
668        }
669
670        if !reader.is_finished() {
671            return Err(Error::TrailingData {
672                remaining: reader.remaining_len(),
673            });
674        }
675
676        Ok(Self {
677            cipher: Cipher::None,
678            kdf: Kdf::None,
679            checkint: Some(checkint1),
680            public_key,
681            key_data,
682            auth_tag: None,
683        })
684    }
685
686    /// Encode [`KeypairData`] along with its associated checkints, comment,
687    /// and padding.
688    fn encode_privatekey_comment_pair(
689        &self,
690        writer: &mut impl Writer,
691        cipher: Cipher,
692        checkint: u32,
693    ) -> encoding::Result<()> {
694        let unpadded_len = self.unpadded_privatekey_comment_pair_len()?;
695        let padding_len = cipher.padding_len(unpadded_len);
696
697        checkint.encode(writer)?;
698        checkint.encode(writer)?;
699        self.key_data.encode(writer)?;
700
701        // Serialize comment
702        #[cfg(not(feature = "alloc"))]
703        b"".encode(writer)?;
704        #[cfg(feature = "alloc")]
705        self.comment().encode(writer)?;
706
707        writer.write(&PADDING_BYTES[..padding_len])?;
708        Ok(())
709    }
710
711    /// Get the length of this private key when encoded with the given comment
712    /// and padded using the padding size for the given cipher.
713    fn encoded_privatekey_comment_pair_len(&self, cipher: Cipher) -> encoding::Result<usize> {
714        let len = self.unpadded_privatekey_comment_pair_len()?;
715        [len, cipher.padding_len(len)].checked_sum()
716    }
717
718    /// Get the length of this private key when encoded with the given comment.
719    ///
720    /// This length is just the checkints, private key data, and comment sans
721    /// any padding.
722    fn unpadded_privatekey_comment_pair_len(&self) -> encoding::Result<usize> {
723        // This method is intended for use with unencrypted keys only
724        debug_assert!(!self.is_encrypted(), "called on encrypted key");
725
726        #[cfg(not(feature = "alloc"))]
727        let comment_len = 0;
728        #[cfg(feature = "alloc")]
729        let comment_len = self.comment().encoded_len()?;
730
731        [
732            8, // 2 x uint32 checkints,
733            self.key_data.encoded_len()?,
734            comment_len,
735        ]
736        .checked_sum()
737    }
738}
739
740impl ConstantTimeEq for PrivateKey {
741    fn ct_eq(&self, other: &Self) -> Choice {
742        // Constant-time with respect to private key data
743        self.key_data.ct_eq(&other.key_data)
744            & Choice::from(
745                (self.cipher == other.cipher
746                    && self.kdf == other.kdf
747                    && self.public_key == other.public_key) as u8,
748            )
749    }
750}
751
752impl Eq for PrivateKey {}
753
754impl PartialEq for PrivateKey {
755    fn eq(&self, other: &Self) -> bool {
756        self.ct_eq(other).into()
757    }
758}
759
760impl Decode for PrivateKey {
761    type Error = Error;
762
763    fn decode(reader: &mut impl Reader) -> Result<Self> {
764        let mut auth_magic = [0u8; Self::AUTH_MAGIC.len()];
765        reader.read(&mut auth_magic)?;
766
767        if auth_magic != Self::AUTH_MAGIC {
768            return Err(Error::FormatEncoding);
769        }
770
771        let cipher = Cipher::decode(reader)?;
772        let kdf = Kdf::decode(reader)?;
773        let nkeys = usize::decode(reader)?;
774
775        // TODO(tarcieri): support more than one key?
776        if nkeys != 1 {
777            return Err(encoding::Error::Length.into());
778        }
779
780        let public_key = reader.read_prefixed(public::KeyData::decode)?;
781
782        // Handle encrypted private key
783        #[cfg(not(feature = "alloc"))]
784        if cipher.is_some() {
785            return Err(Error::Encrypted);
786        }
787        #[cfg(feature = "alloc")]
788        if cipher.is_some() {
789            let ciphertext = Vec::decode(reader)?;
790
791            // Ensure ciphertext is padded to the expected length
792            if ciphertext.len().checked_rem(cipher.block_size()) != Some(0) {
793                return Err(Error::Crypto);
794            }
795
796            let auth_tag = if cipher.has_tag() {
797                let mut tag = Tag::default();
798                reader.read(&mut tag)?;
799                Some(tag)
800            } else {
801                None
802            };
803
804            if !reader.is_finished() {
805                return Err(Error::TrailingData {
806                    remaining: reader.remaining_len(),
807                });
808            }
809
810            return Ok(Self {
811                cipher,
812                kdf,
813                checkint: None,
814                public_key: public_key.into(),
815                key_data: KeypairData::Encrypted(ciphertext),
816                auth_tag,
817            });
818        }
819
820        // Processing unencrypted key. No KDF should be set.
821        if kdf.is_some() {
822            return Err(Error::Crypto);
823        }
824
825        reader.read_prefixed(|reader| {
826            // PuTTYgen uses a non-standard block size of 16
827            // and _always_ adds a padding even if data length
828            // is divisible by 16 - for unencrypted keys
829            // in the OpenSSH format.
830            // We're only relaxing the exact length check, but will
831            // still validate that the contents of the padding area.
832            // In all other cases there can be up to (but not including)
833            // `block_size` padding bytes as per `PROTOCOL.key`.
834            let max_padding_size = match cipher {
835                Cipher::None => 16,
836                #[allow(clippy::arithmetic_side_effects)] // block sizes are constants
837                _ => cipher.block_size() - 1,
838            };
839            Self::decode_privatekey_comment_pair(
840                reader,
841                public_key,
842                cipher.block_size(),
843                max_padding_size,
844            )
845        })
846    }
847}
848
849impl Encode for PrivateKey {
850    fn encoded_len(&self) -> encoding::Result<usize> {
851        let private_key_len = if self.is_encrypted() {
852            self.key_data.encoded_len_prefixed()?
853        } else {
854            [4, self.encoded_privatekey_comment_pair_len(Cipher::None)?].checked_sum()?
855        };
856
857        [
858            Self::AUTH_MAGIC.len(),
859            self.cipher.encoded_len()?,
860            self.kdf.encoded_len()?,
861            4, // number of keys (uint32)
862            self.public_key.key_data().encoded_len_prefixed()?,
863            private_key_len,
864            self.auth_tag.map(|tag| tag.len()).unwrap_or(0),
865        ]
866        .checked_sum()
867    }
868
869    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
870        writer.write(Self::AUTH_MAGIC)?;
871        self.cipher.encode(writer)?;
872        self.kdf.encode(writer)?;
873
874        // TODO(tarcieri): support for encoding more than one private key
875        1usize.encode(writer)?;
876
877        // Encode public key
878        self.public_key.key_data().encode_prefixed(writer)?;
879
880        // Encode private key
881        if self.is_encrypted() {
882            self.key_data.encode_prefixed(writer)?;
883
884            if let Some(tag) = &self.auth_tag {
885                writer.write(tag)?;
886            }
887        } else {
888            self.encoded_privatekey_comment_pair_len(Cipher::None)?
889                .encode(writer)?;
890
891            let checkint = self.checkint.unwrap_or_else(|| self.key_data.checkint());
892            self.encode_privatekey_comment_pair(writer, Cipher::None, checkint)?;
893        }
894
895        Ok(())
896    }
897}
898
899impl From<PrivateKey> for PublicKey {
900    fn from(private_key: PrivateKey) -> PublicKey {
901        private_key.public_key
902    }
903}
904
905impl From<&PrivateKey> for PublicKey {
906    fn from(private_key: &PrivateKey) -> PublicKey {
907        private_key.public_key.clone()
908    }
909}
910
911impl From<PrivateKey> for public::KeyData {
912    fn from(private_key: PrivateKey) -> public::KeyData {
913        private_key.public_key.key_data
914    }
915}
916
917impl From<&PrivateKey> for public::KeyData {
918    fn from(private_key: &PrivateKey) -> public::KeyData {
919        private_key.public_key.key_data.clone()
920    }
921}
922
923#[cfg(feature = "alloc")]
924impl From<DsaKeypair> for PrivateKey {
925    fn from(keypair: DsaKeypair) -> PrivateKey {
926        KeypairData::from(keypair)
927            .try_into()
928            .expect(CONVERSION_ERROR_MSG)
929    }
930}
931
932#[cfg(feature = "ecdsa")]
933impl From<EcdsaKeypair> for PrivateKey {
934    fn from(keypair: EcdsaKeypair) -> PrivateKey {
935        KeypairData::from(keypair)
936            .try_into()
937            .expect(CONVERSION_ERROR_MSG)
938    }
939}
940
941impl From<Ed25519Keypair> for PrivateKey {
942    fn from(keypair: Ed25519Keypair) -> PrivateKey {
943        KeypairData::from(keypair)
944            .try_into()
945            .expect(CONVERSION_ERROR_MSG)
946    }
947}
948
949#[cfg(feature = "alloc")]
950impl From<RsaKeypair> for PrivateKey {
951    fn from(keypair: RsaKeypair) -> PrivateKey {
952        KeypairData::from(keypair)
953            .try_into()
954            .expect(CONVERSION_ERROR_MSG)
955    }
956}
957
958#[cfg(all(feature = "alloc", feature = "ecdsa"))]
959impl From<SkEcdsaSha2NistP256> for PrivateKey {
960    fn from(keypair: SkEcdsaSha2NistP256) -> PrivateKey {
961        KeypairData::from(keypair)
962            .try_into()
963            .expect(CONVERSION_ERROR_MSG)
964    }
965}
966
967#[cfg(feature = "alloc")]
968impl From<SkEd25519> for PrivateKey {
969    fn from(keypair: SkEd25519) -> PrivateKey {
970        KeypairData::from(keypair)
971            .try_into()
972            .expect(CONVERSION_ERROR_MSG)
973    }
974}
975
976impl TryFrom<KeypairData> for PrivateKey {
977    type Error = Error;
978
979    fn try_from(key_data: KeypairData) -> Result<PrivateKey> {
980        let public_key = public::KeyData::try_from(&key_data)?;
981
982        Ok(Self {
983            cipher: Cipher::None,
984            kdf: Kdf::None,
985            checkint: None,
986            public_key: public_key.into(),
987            key_data,
988            auth_tag: None,
989        })
990    }
991}
992
993impl PemLabel for PrivateKey {
994    const PEM_LABEL: &'static str = "OPENSSH PRIVATE KEY";
995}
996
997impl str::FromStr for PrivateKey {
998    type Err = Error;
999
1000    fn from_str(s: &str) -> Result<Self> {
1001        Self::from_openssh(s)
1002    }
1003}