use crate::consts::{
COMMENT_HEADER, COMMENT_MAX_LEN, FULL_KEY_LEN, PKGALG, PUBLIC_KEY_LEN, SIG_LEN,
};
use crate::errors::{Error, FormatError};
use crate::{KeyNumber, PrivateKey, PublicKey, Signature};
use alloc::vec::Vec;
use base64ct::Encoding;
use zeroize::Zeroizing;
pub trait Codeable: Sized + Sealed {
fn from_bytes(bytes: &[u8]) -> Result<Self, Error>;
fn from_base64(encoded: &str) -> Result<(Self, u64), Error> {
read_base64_contents(encoded).and_then(|(bytes, bytes_read)| {
let bytes = Self::from_bytes(&bytes)?;
Ok((bytes, bytes_read))
})
}
fn as_bytes(&self) -> Vec<u8>;
fn to_file_encoding(&self, comment: &str) -> Vec<u8> {
let bytes = self.as_bytes();
let mut file_bytes = Vec::new();
file_bytes.extend_from_slice(COMMENT_HEADER.as_bytes());
file_bytes.extend_from_slice(comment.as_bytes());
file_bytes.push(b'\n');
let out = base64ct::Base64::encode_string(&bytes);
file_bytes.extend_from_slice(out.as_bytes());
file_bytes.push(b'\n');
file_bytes
}
}
use sealed::Sealed;
mod sealed {
pub trait Sealed {}
}
impl Sealed for PublicKey {}
impl Sealed for PrivateKey {}
impl Sealed for Signature {}
struct SliceReader<'a>(&'a [u8]);
impl SliceReader<'_> {
fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> {
if buf.len() > self.0.len() {
return Err(Error::InsufficentData);
}
let (a, b) = self.0.split_at(buf.len());
buf.copy_from_slice(a);
self.0 = b;
Ok(())
}
}
impl Codeable for PublicKey {
fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
let mut buf = SliceReader(bytes);
let mut _pkgalg = [0u8; 2];
let mut keynum = [0; KeyNumber::LEN];
let mut public_key = [0; PUBLIC_KEY_LEN];
buf.read_exact(&mut _pkgalg)?;
buf.read_exact(&mut keynum)?;
buf.read_exact(&mut public_key)?;
Ok(Self {
keynum: KeyNumber::new(keynum),
key: public_key,
})
}
fn as_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&PKGALG);
bytes.extend_from_slice(self.keynum.as_ref());
bytes.extend_from_slice(&self.key);
bytes
}
}
impl Codeable for PrivateKey {
fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
let mut buf = SliceReader(bytes);
let mut public_key_alg = [0; 2];
let mut kdf_alg = [0; 2];
let mut salt = [0; 16];
let mut checksum = [0; 8];
let mut keynum = [0; KeyNumber::LEN];
let mut complete_key = Zeroizing::new([0; FULL_KEY_LEN]);
buf.read_exact(&mut public_key_alg)?;
buf.read_exact(&mut kdf_alg)?;
let kdf_rounds = {
let mut bytes = [0u8; core::mem::size_of::<u32>()];
buf.read_exact(&mut bytes)?;
u32::from_be_bytes(bytes)
};
buf.read_exact(&mut salt)?;
buf.read_exact(&mut checksum)?;
buf.read_exact(&mut keynum)?;
buf.read_exact(&mut complete_key[..])?;
if kdf_rounds == 0 {
PrivateKey::from_key_bytes(&complete_key).map(drop)?;
}
Ok(Self {
public_key_alg,
kdf_alg,
kdf_rounds,
salt,
checksum,
keynum: KeyNumber::new(keynum),
complete_key,
})
}
fn as_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.public_key_alg);
bytes.extend_from_slice(&self.kdf_alg);
bytes.extend_from_slice(&self.kdf_rounds.to_be_bytes());
bytes.extend_from_slice(&self.salt);
bytes.extend_from_slice(&self.checksum);
bytes.extend_from_slice(self.keynum.as_ref());
bytes.extend_from_slice(self.complete_key.as_ref());
bytes
}
}
impl Codeable for Signature {
fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
let mut buf = SliceReader(bytes);
let mut _pkgalg = [0u8; 2];
let mut keynum = [0; KeyNumber::LEN];
let mut sig = [0; SIG_LEN];
buf.read_exact(&mut _pkgalg)?;
buf.read_exact(&mut keynum)?;
buf.read_exact(&mut sig)?;
Ok(Self {
keynum: KeyNumber::new(keynum),
sig,
})
}
fn as_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&PKGALG);
bytes.extend_from_slice(self.keynum.as_ref());
bytes.extend_from_slice(&self.sig);
bytes
}
}
fn read_base64_contents(encoded: &str) -> Result<(Vec<u8>, u64), Error> {
let mut lines = encoded.split_terminator('\n');
let comment_line = lines.next().ok_or(FormatError::MissingNewline)?;
if !comment_line.starts_with(COMMENT_HEADER) {
return Err(FormatError::Comment {
expected: COMMENT_HEADER,
}
.into());
}
if comment_line.len() > COMMENT_HEADER.len() + COMMENT_MAX_LEN {
return Err(FormatError::LineLength.into());
}
let base64_line = lines.next().ok_or(FormatError::MissingNewline)?;
let data =
base64ct::Base64::decode_vec(base64_line.trim_end()).map_err(|_| FormatError::Base64)?;
match data.get(0..2) {
Some(alg) if alg == PKGALG => {
let bytes_read = comment_line.len() + base64_line.len() + 2;
Ok((data, bytes_read as u64))
}
Some(_) | None => Err(Error::UnsupportedAlgorithm),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NewKeyOpts;
use alloc::string::String;
#[test]
fn public_key_codeable() {
let key = PublicKey {
keynum: KeyNumber::new([3u8; KeyNumber::LEN]),
key: [4u8; PUBLIC_KEY_LEN],
};
let bytes = key.as_bytes();
let deserialized = PublicKey::from_bytes(&bytes).unwrap();
assert_eq!(key, deserialized);
let file_encoding = String::from_utf8(key.to_file_encoding("my comment")).unwrap();
let (deserialized, bytes_read) = PublicKey::from_base64(&file_encoding).unwrap();
assert_eq!(key, deserialized);
assert!(bytes_read > 0);
}
#[test]
fn private_key_codeable() {
let mut rng = rand_core::OsRng;
{
let key = PrivateKey::generate(
&mut rng,
NewKeyOpts::Encrypted {
passphrase: String::from("supersecure"),
kdf_rounds: 16,
},
)
.unwrap();
let bytes = key.as_bytes();
let deserialized = PrivateKey::from_bytes(&bytes).unwrap();
assert_eq!(deserialized.complete_key, key.complete_key);
}
let key = PrivateKey::generate(&mut rng, NewKeyOpts::NoEncryption).unwrap();
let bytes = key.as_bytes();
let deserialized = PrivateKey::from_bytes(&bytes).unwrap();
assert_eq!(key, deserialized);
let file_encoding = String::from_utf8(key.to_file_encoding("my comment")).unwrap();
let (deserialized, bytes_read) = PrivateKey::from_base64(&file_encoding).unwrap();
assert_eq!(key, deserialized);
assert!(bytes_read > 0);
}
#[test]
fn signature_codeable() {
let sig = Signature {
keynum: KeyNumber::new([3u8; KeyNumber::LEN]),
sig: [9u8; SIG_LEN],
};
let bytes = sig.as_bytes();
let deserialized = Signature::from_bytes(&bytes).unwrap();
assert_eq!(sig, deserialized);
let file_encoding = String::from_utf8(sig.to_file_encoding("my comment")).unwrap();
let (deserialized, bytes_read) = Signature::from_base64(&file_encoding).unwrap();
assert_eq!(sig, deserialized);
assert!(bytes_read > 0);
}
const BASE64_CASES: &[(&str, Result<(), Error>)] = &[
(
"untrusted comment: my comment\nRWQgaW4gV29uZGVybGFuZA==\n",
Ok(()),
),
(
"nottherightheader: aaaaaaa",
Err(Error::InvalidFormat(FormatError::Comment {
expected: COMMENT_HEADER,
})),
),
(
"untrusted comment: makethisverylong",
Err(Error::InvalidFormat(FormatError::LineLength)),
),
(
"untrusted comment: bbbbbb\rbbbbbbbb",
Err(Error::InvalidFormat(FormatError::MissingNewline)),
),
(
"untrusted comment: cccccc\n",
Err(Error::InvalidFormat(FormatError::MissingNewline)),
),
(
"untrusted comment: dddddd\nRWQgaW4gV2ZGVybGFuZA==",
Err(Error::InvalidFormat(FormatError::Base64)),
),
(
"untrusted comment: eeeeee\nUGF0IGluIFdvbmRlcmxhbmQ=",
Err(Error::UnsupportedAlgorithm),
),
];
#[test]
fn base64_reading() {
for (encoded, expected) in BASE64_CASES {
let encoded = encoded.replace("makethisverylong", &"a".repeat(1025));
let result = read_base64_contents(&encoded).map(|_| ());
assert_eq!(result, *expected, "{} produced the wrong result", encoded);
}
}
}