use anyhow::Context;
use openpgp::parse::buffered_reader::{self, BufferedReader};
use openpgp::cert::Cert;
use openpgp::parse::{Cookie, Parse};
use openpgp::types::HashAlgorithm::SHA1;
use openpgp::Result;
use sequoia_openpgp as openpgp;
use std::convert::TryInto;
use std::fmt::Display;
pub struct Keybox<'a> {
offset: usize,
reader: Box<dyn BufferedReader<()> + 'a>,
}
impl<'a> Keybox<'a> {
fn read_next_record(&mut self) -> Result<KeyboxRecord> {
let input = self
.reader
.data_hard(6)
.map_err(|e| Error::NotEnoughData(e.to_string()))?;
let len = u32::from_be_bytes(input[..4].try_into().unwrap()) as usize;
let content = self.reader.data_consume_hard(len)?;
let offset = self.offset;
self.offset += len;
let kbx_record = KeyboxRecord::new(offset, (&content[..len]).to_vec())?;
Ok(kbx_record)
}
pub fn from_buffered_reader<R>(reader: R) -> Result<Self>
where
R: BufferedReader<Cookie> + 'a,
{
Ok(Keybox {
offset: 0,
reader: buffered_reader::Adapter::new(reader).into_boxed(),
})
}
pub fn from_reader<R: 'a + std::io::Read + Send + Sync>(reader: R) -> Result<Self> {
Self::from_buffered_reader(
buffered_reader::Generic::with_cookie(reader,
None,
Default::default())
.into_boxed())
}
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self>
{
Self::from_buffered_reader(
buffered_reader::File::with_cookie(path.as_ref(),
Default::default())?
.into_boxed())
}
pub fn from_bytes<D: AsRef<[u8]> + ?Sized + Send + Sync>(data: &'a D) -> Result<Self> {
Self::from_buffered_reader(
buffered_reader::Memory::with_cookie(data.as_ref(), Default::default())
.into_boxed())
}
}
impl<'a> Iterator for Keybox<'a> {
type Item = Result<KeyboxRecord>;
fn next(&mut self) -> Option<Self::Item> {
if self.reader.eof() {
None
} else {
Some(self.read_next_record())
}
}
}
#[non_exhaustive]
#[derive(PartialEq, Eq, Debug)]
pub enum KeyboxRecordType {
Header,
OpenPGP,
X509,
Unknown(u8),
}
impl From<u8> for KeyboxRecordType {
fn from(value: u8) -> Self {
match value {
1 => KeyboxRecordType::Header,
2 => KeyboxRecordType::OpenPGP,
3 => KeyboxRecordType::X509,
v => KeyboxRecordType::Unknown(v),
}
}
}
impl Display for KeyboxRecordType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyboxRecordType::Header => write!(f, "Header"),
KeyboxRecordType::OpenPGP => write!(f, "OpenPGP"),
KeyboxRecordType::X509 => write!(f, "X509"),
KeyboxRecordType::Unknown(v) => write!(f, "Unknown: {}", v),
}
}
}
impl KeyboxRecord {
fn bytes(&self) -> &[u8] {
match self {
KeyboxRecord::Header(h) => &h.bytes,
KeyboxRecord::OpenPGP(o) => &o.bytes,
KeyboxRecord::X509(x) => &x.bytes,
KeyboxRecord::Unknown(_, bytes) => bytes,
}
}
pub fn offset(&self) -> usize {
match self {
KeyboxRecord::Header(h) => h.offset(),
KeyboxRecord::OpenPGP(o) => o.offset(),
KeyboxRecord::X509(x) => x.offset(),
KeyboxRecord::Unknown(offset, _bytes) => *offset,
}
}
pub fn length_field(&self) -> u32 {
u32::from_be_bytes((&self.bytes()[..4]).try_into().unwrap())
}
pub fn typ(&self) -> KeyboxRecordType {
KeyboxRecordType::from(self.bytes()[4])
}
pub fn version(&self) -> u8 {
self.bytes()[5]
}
fn new(offset: usize, bytes: Vec<u8>) -> Result<Self> {
if bytes.len() < 6 {
return Err(Error::NotEnoughData(
"A keybox record requires at least 6 bytes.".to_string(),
)
.into());
}
let record = KeyboxRecord::Unknown(offset, bytes.clone());
match record.typ() {
KeyboxRecordType::Header => {
HeaderRecord::new(offset, bytes).map(KeyboxRecord::Header)
}
KeyboxRecordType::OpenPGP => {
OpenPGPRecordV1::new(offset, &record).map(KeyboxRecord::OpenPGP)
}
KeyboxRecordType::X509 => {
X509Record::new(offset, bytes).map(KeyboxRecord::X509)
}
KeyboxRecordType::Unknown(_) => Ok(record),
}
}
}
#[non_exhaustive]
#[derive(PartialEq, Eq, Debug)]
pub enum KeyboxRecord {
Header(HeaderRecord),
OpenPGP(OpenPGPRecordV1),
X509(X509Record),
Unknown(usize, Vec<u8>),
}
#[derive(PartialEq, Eq, Debug)]
pub struct HeaderRecord {
offset: usize,
bytes: Vec<u8>,
}
impl HeaderRecord {
fn new(offset: usize, bytes: Vec<u8>) -> Result<Self> {
Ok(Self { offset, bytes })
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn flags(&self) -> [u8; 2] {
self.bytes[0x6..=0x7].try_into().unwrap()
}
pub fn check_magic(&self) -> bool {
let magic = &self.bytes[0x08..=0x0B];
magic == b"KBXf"
}
pub fn created_at(&self) -> u32 {
u32::from_be_bytes((self.bytes[0x10..=0x13]).try_into().unwrap())
}
pub fn last_maintained(&self) -> u32 {
u32::from_be_bytes((self.bytes[0x14..=0x17]).try_into().unwrap())
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct X509Record {
offset: usize,
bytes: Vec<u8>,
}
impl X509Record {
fn new(offset: usize, bytes: Vec<u8>) -> Result<Self> {
Ok(Self { offset, bytes })
}
pub fn offset(&self) -> usize {
self.offset
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct OpenPGPRecordV1 {
offset: usize,
bytes: Vec<u8>,
}
impl OpenPGPRecordV1 {
fn new(offset: usize, record: &KeyboxRecord) -> Result<Self> {
if record.typ() != KeyboxRecordType::OpenPGP || record.version() != 1 {
return Err(
Error::UnhandledRecord(record.typ(), record.version()).into()
);
}
if record.bytes().len() < 0x10 {
return Err(Error::NotEnoughData(format!(
"OpenPGP record header is 16 bytes, got {}",
record.bytes().len()
))
.into());
};
let record = OpenPGPRecordV1 {
offset,
bytes: record.bytes().to_vec(),
};
if record.checksum_field()[..] != record.compute_checksum()? {
return Err(Error::InvalidData("wrong checksum".to_string()).into());
}
Ok(record)
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn flags(&self) -> [u8; 2] {
self.bytes[0x6..=0x7].try_into().unwrap()
}
pub fn data_offset(&self) -> usize {
u32::from_be_bytes((self.bytes[0x8..=0xB]).try_into().unwrap()) as usize
}
pub fn data_length(&self) -> usize {
u32::from_be_bytes((self.bytes[0xC..=0xF]).try_into().unwrap()) as usize
}
pub fn data_section(&self) -> Result<&[u8]> {
let data_end = self.data_offset() + self.data_length();
if self.bytes.len() < data_end {
return Err(Error::NotEnoughData(
"data section truncated".to_string(),
)
.into());
};
Ok(&self.bytes[self.data_offset()..data_end])
}
pub fn metadata_section(&self) -> &[u8] {
&self.bytes[0x10..self.data_offset()]
}
pub fn checksum_field(&self) -> [u8; 20] {
let hash_offset = self.data_offset() + self.data_length();
self.bytes[hash_offset..hash_offset + 20]
.try_into()
.unwrap()
}
pub fn compute_checksum(&self) -> Result<Vec<u8>> {
let hash_offset = self.data_offset() + self.data_length();
let (hashed_data, _hash) = &self.bytes.split_at(hash_offset);
let mut ctx = SHA1.context()?.for_digest();
ctx.update(hashed_data);
ctx.into_digest()
}
pub fn cert(&self) -> Result<Cert> {
let cert_data = &self.data_section()?;
Cert::from_bytes(cert_data)
.with_context(
|| format!("Parsing keybox record at offset {}",
self.offset()))
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Not enough data: {0}")]
NotEnoughData(String),
#[error("Unhandled record type: {0}, version {1}")]
UnhandledRecord(KeyboxRecordType, u8),
#[error("Invalid data: {0}")]
InvalidData(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
fn parse_keybox(reader: &mut dyn BufferedReader<()>) -> Result<Vec<Cert>> {
let kbx = Keybox::from_reader(reader)?;
let certs = kbx
.filter_map(|kbx_record| kbx_record.ok())
.filter_map(|kbx_record| match kbx_record {
KeyboxRecord::OpenPGP(r) => Some(r.cert()),
_ => None,
})
.collect::<Result<Vec<Cert>>>();
certs
}
#[test]
fn keybox_record() -> Result<()> {
let header_bytes = crate::tests::keybox("header_sample");
let header_kbx = KeyboxRecord::new(0, header_bytes.to_vec())?;
assert_eq!(header_kbx.typ(), header_bytes[4].into());
let openpgp_bytes = crate::tests::keybox("testy_openpgp");
let openpgp_kbx = KeyboxRecord::new(0, openpgp_bytes.to_vec())?;
assert_eq!(openpgp_kbx.typ(), openpgp_bytes[4].into());
let x509_bytes = crate::tests::keybox("testy_x509");
let x509_kbx = KeyboxRecord::new(0, x509_bytes.to_vec())?;
assert_eq!(x509_kbx.typ(), x509_bytes[4].into());
let too_short = &[1u8; 5];
assert!(KeyboxRecord::new(0, too_short.to_vec()).is_err());
Ok(())
}
#[test]
fn cert_from_openpgp_record() -> Result<()> {
let openpgp_bytes = crate::tests::keybox("testy_openpgp");
let kbx_record = KeyboxRecord::new(0, openpgp_bytes.to_vec())?;
let openpgp_record = match kbx_record {
KeyboxRecord::OpenPGP(r) => r,
_ => unreachable!(),
};
let cert = openpgp_record.cert().unwrap();
let testy = Cert::from_bytes(crate::tests::key("testy.pgp")).unwrap();
assert_eq!(cert, testy);
Ok(())
}
#[test]
fn cert_from_keybox() -> Result<()> {
let bytes = crate::tests::keybox("keybox.kbx");
let mut br = buffered_reader::Memory::new(bytes);
let certs = parse_keybox(&mut br)?;
let testy = Cert::from_bytes(crate::tests::key("testy.pgp"))?;
assert_eq!(certs[0], testy);
Ok(())
}
#[test]
fn openpgp_record() -> Result<()> {
let openpgp_bytes = crate::tests::keybox("testy_openpgp");
let kbx_record = KeyboxRecord::new(0, openpgp_bytes.to_vec())?;
assert_eq!(kbx_record.length_field(), 1428u32);
assert_eq!(kbx_record.typ(), KeyboxRecordType::OpenPGP);
assert_eq!(kbx_record.version(), 1u8);
let openpgp_record = match kbx_record {
KeyboxRecord::OpenPGP(r) => r,
_ => unreachable!(),
};
assert_eq!(openpgp_record.flags(), [0u8, 0u8]);
assert_eq!(openpgp_record.data_offset(), 126usize);
assert_eq!(openpgp_record.data_length(), 1282usize);
assert_eq!(
openpgp_record.metadata_section(),
[
0, 2, 0, 28, 62, 136, 119, 200, 119, 39, 70, 146, 151, 81, 137,
245, 208, 63, 111, 134, 82, 38, 254, 139, 0, 0, 0, 32, 0, 0, 0,
0, 1, 241, 135, 87, 91, 212, 86, 68, 4, 101, 100, 193, 73, 226,
17, 129, 102, 201, 38, 50, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 1,
0, 12, 0, 0, 1, 158, 0, 0, 0, 36, 0, 0, 0, 0, 0, 2, 0, 4, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 129,
142, 142, 0, 0, 0, 0
]
);
Ok(())
}
#[test]
fn openpgp_errors() -> Result<()> {
let openpgp_too_short = [0u8, 7u8, 1u8, 1u8, 2u8, 1u8, 1u8];
assert!(KeyboxRecord::new(0, openpgp_too_short.to_vec()).is_err());
let openpgp_unknown_version = [0u8, 7u8, 1u8, 1u8, 2u8, 7u8, 1u8];
assert!(KeyboxRecord::new(0, openpgp_unknown_version.to_vec()).is_err());
let mut openpgp_wrong_checksum = crate::tests::keybox("testy_openpgp").to_vec();
if let Some(last) = openpgp_wrong_checksum.last_mut() {
*last = 0u8;
};
assert!(KeyboxRecord::new(0, openpgp_wrong_checksum.to_vec()).is_err());
Ok(())
}
#[test]
fn header_record() -> Result<()> {
let header_bytes = crate::tests::keybox("header_sample");
let kbx_record = KeyboxRecord::new(0, header_bytes.to_vec())?;
assert_eq!(kbx_record.length_field(), 32u32);
assert_eq!(kbx_record.typ(), KeyboxRecordType::Header);
assert_eq!(kbx_record.version(), 1u8);
let header_record = match kbx_record {
KeyboxRecord::Header(r) => r,
_ => unreachable!(),
};
assert!(header_record.check_magic());
assert_eq!(header_record.flags(), [0x00u8, 0x02u8]);
assert_eq!(header_record.created_at(), 0x6081_8e8eu32);
assert_eq!(header_record.last_maintained(), 0x6081_8e8eu32);
Ok(())
}
#[test]
fn x509_record() -> Result<()> {
let x509_bytes = crate::tests::keybox("testy_x509");
let kbx_record = KeyboxRecord::new(0, x509_bytes.to_vec())?;
assert_eq!(kbx_record.length_field(), 1704u32);
assert_eq!(kbx_record.typ(), KeyboxRecordType::X509);
assert_eq!(kbx_record.version(), 1u8);
Ok(())
}
}