Compare commits

..

3 Commits

25 changed files with 146 additions and 27 deletions

View File

@ -11,7 +11,7 @@ fn secp256k1_test_suite() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove(&"secp256k1".to_string()) .remove("secp256k1")
.unwrap(); .unwrap();
for seed_test in tests { for seed_test in tests {
@ -70,7 +70,7 @@ fn secp256k1_test_suite() {
fn ed25519_test_suite() { fn ed25519_test_suite() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap(); let tests = test_data().unwrap().remove("ed25519").unwrap();
for seed_test in tests { for seed_test in tests {
let seed = seed_test.seed; let seed = seed_test.seed;

View File

@ -1,4 +1,4 @@
//! //! Launch the Keyfork Server from using a mnemonic passed through standard input.
use keyfork_mnemonic::Mnemonic; use keyfork_mnemonic::Mnemonic;

View File

@ -113,7 +113,7 @@ mod tests {
async fn properly_derives_secp256k1() { async fn properly_derives_secp256k1() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove(&"secp256k1".to_string()) .remove("secp256k1")
.unwrap(); .unwrap();
for per_seed in tests { for per_seed in tests {
@ -146,7 +146,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn properly_derives_ed25519() { async fn properly_derives_ed25519() {
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap(); let tests = test_data().unwrap().remove("ed25519").unwrap();
for per_seed in tests { for per_seed in tests {
let seed = &per_seed.seed; let seed = &per_seed.seed;

View File

@ -0,0 +1,16 @@
[package]
name = "keyfork-derive-age"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
keyforkd-client = { workspace = true }
smex = { workspace = true }
thiserror = "1.0.48"
bech32 = "0.11.0"
keyfork-derive-path-data = { workspace = true }
ed25519-dalek = "2.1.1"

View File

@ -0,0 +1,69 @@
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{DerivationPath, ExtendedPrivateKey, PathError};
use keyforkd_client::Client;
use ed25519_dalek::SigningKey;
type XPrv = ExtendedPrivateKey<SigningKey>;
/// Any error that can occur while deriving a key.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The given path could not be parsed.
#[error("Could not parse the given path: {0}")]
PathFormat(#[from] PathError),
/// The request to derive data failed.
#[error("Unable to perform key derivation request: {0}")]
KeyforkdClient(#[from] keyforkd_client::Error),
}
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
fn validate(path: &str) -> Result<DerivationPath> {
let index = paths::AGE.inner().first().unwrap();
let path = DerivationPath::from_str(path)?;
assert!(
path.len() >= 2,
"Expected path of at least m/{index}/account_id'"
);
let given_index = path.iter().next().expect("checked .len() above");
assert_eq!(
index, given_index,
"Expected derivation path starting with m/{index}, got: {given_index}",
);
Ok(path)
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let path = match args.as_slice() {
[path] => validate(path)?,
_ => panic!("Usage: {program_name} path"),
};
let mut client = Client::discover_socket()?;
// TODO: should this key be clamped to Curve25519 specs?
let xprv: XPrv = client.request_xprv(&path)?;
let hrp = bech32::Hrp::parse("AGE-SECRET-KEY-")?;
let age_key = bech32::encode::<bech32::Bech32>(hrp, &xprv.private_key().to_bytes())?;
println!("{}", age_key.to_uppercase());
Ok(())
}
fn main() -> ExitCode {
if let Err(e) = run() {
eprintln!("Error: {e}");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}

View File

@ -1,4 +1,4 @@
//! //! Query the Keyfork Server to generate a hex-encoded key for a given algorithm.
use std::{env, process::ExitCode, str::FromStr}; use std::{env, process::ExitCode, str::FromStr};

View File

@ -19,8 +19,14 @@ use sequoia_openpgp::{
Cert, Packet, Cert, Packet,
}; };
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
// something like Prv or PrvKey.
/// The private key type used with OpenPGP.
pub type XPrvKey = SigningKey; pub type XPrvKey = SigningKey;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// The extended private key type used with OpenPGP.
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
/// An error occurred while creating an OpenPGP key. /// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View File

@ -1,4 +1,4 @@
//! //! Query the Keyfork Servre to derive an OpenPGP Secret Key.
use std::{env, process::ExitCode, str::FromStr}; use std::{env, process::ExitCode, str::FromStr};

View File

@ -6,6 +6,7 @@ use once_cell::sync::Lazy;
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
/// All common paths for key derivation.
pub mod paths { pub mod paths {
use super::*; use super::*;

View File

@ -41,9 +41,9 @@
//! } //! }
//! ``` //! ```
/// #[allow(missing_docs)]
pub mod private_key; pub mod private_key;
/// #[allow(missing_docs)]
pub mod public_key; pub mod public_key;
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey}; pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};

View File

@ -15,7 +15,7 @@ fn secp256k1() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove(&"secp256k1".to_string()) .remove("secp256k1")
.unwrap(); .unwrap();
for per_seed in tests { for per_seed in tests {
@ -62,7 +62,7 @@ fn secp256k1() {
fn ed25519() { fn ed25519() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap(); let tests = test_data().unwrap().remove("ed25519").unwrap();
for per_seed in tests { for per_seed in tests {
let seed = &per_seed.seed; let seed = &per_seed.seed;

View File

@ -1,4 +1,4 @@
//! //! Combine OpenPGP shards and output the hex-encoded secret.
use std::{ use std::{
env, env,

View File

@ -1,4 +1,4 @@
//! //! Decrypt a single OpenPGP shard and encapsulate it for remote transport.
use std::{ use std::{
env, env,

View File

@ -1,4 +1,4 @@
//! //! Combine OpenPGP shards using remote transport and output the hex-encoded secret.
use std::{ use std::{
env, env,

View File

@ -1,4 +1,4 @@
//! //! Split a hex-encoded secret into OpenPGP shards
use std::{env, path::PathBuf, process::ExitCode, str::FromStr}; use std::{env, path::PathBuf, process::ExitCode, str::FromStr};

View File

@ -25,7 +25,7 @@ use openpgp::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper}, stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse, Parse,
}, },
policy::{NullPolicy, Policy}, policy::{NullPolicy, StandardPolicy, Policy},
serialize::{ serialize::{
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer}, stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal, Marshal,
@ -77,6 +77,10 @@ pub enum Error {
/// An IO error occurred. /// An IO error occurred.
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[source] std::io::Error), Io(#[source] std::io::Error),
/// No valid keys were found for the given recipient.
#[error("No valid keys were found for the recipient {0}")]
NoValidKeys(KeyID),
} }
#[allow(missing_docs)] #[allow(missing_docs)]
@ -181,7 +185,7 @@ impl EncryptedMessage {
} }
} }
/// /// Encoding and decoding shards using OpenPGP.
pub struct OpenPGP<P: PromptHandler> { pub struct OpenPGP<P: PromptHandler> {
p: PhantomData<P>, p: PhantomData<P>,
} }
@ -239,6 +243,13 @@ impl<P: PromptHandler> OpenPGP<P> {
certs.insert(certfp, cert); certs.insert(certfp, cert);
} }
} }
for cert in certs.values() {
let policy = StandardPolicy::new();
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
if get_encryption_keys(&valid_cert).next().is_none() {
return Err(Error::NoValidKeys(valid_cert.keyid()))
}
}
Ok(certs.into_values().collect()) Ok(certs.into_values().collect())
} }
} }
@ -276,7 +287,7 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
key_data: &[Self::PublicKey], key_data: &[Self::PublicKey],
threshold: u8, threshold: u8,
) -> Result<Self::EncryptedData, Self::Error> { ) -> Result<Self::EncryptedData, Self::Error> {
let policy = NullPolicy::new(); let policy = StandardPolicy::new();
let mut pp = vec![SHARD_METADATA_VERSION, threshold]; let mut pp = vec![SHARD_METADATA_VERSION, threshold];
// Note: Sequoia does not export private keys on a Cert, only on a TSK // Note: Sequoia does not export private keys on a Cert, only on a TSK
signing_key signing_key
@ -362,7 +373,7 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
public_key: &Cert, public_key: &Cert,
signing_key: &mut Self::SigningKey, signing_key: &mut Self::SigningKey,
) -> Result<EncryptedMessage> { ) -> Result<EncryptedMessage> {
let policy = NullPolicy::new(); let policy = StandardPolicy::new();
let valid_cert = public_key let valid_cert = public_key
.with_policy(&policy, None) .with_policy(&policy, None)
.map_err(Error::Sequoia)?; .map_err(Error::Sequoia)?;

View File

@ -1,4 +1,4 @@
//! #![allow(missing_docs)]
use std::time::Duration; use std::time::Duration;

View File

@ -103,6 +103,11 @@ pub fn qrencode(
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format"; const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
/// Continuously scan the `index`-th camera for a QR code. /// Continuously scan the `index`-th camera for a QR code.
///
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
#[cfg(feature = "decode-backend-rqrr")] #[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;
@ -133,6 +138,11 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
} }
/// Continuously scan the `index`-th camera for a QR code. /// Continuously scan the `index`-th camera for a QR code.
///
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
#[cfg(feature = "decode-backend-zbar")] #[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;

View File

@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.decode()?, .decode()?,
); );
if let Some(symbol) = scanner.scan_image(&image).get(0) { if let Some(symbol) = scanner.scan_image(&image).first() {
println!("{}", String::from_utf8_lossy(symbol.data())); println!("{}", String::from_utf8_lossy(symbol.data()));
return Ok(()); return Ok(());
} }

View File

@ -1,4 +1,4 @@
//! //! A Symbol represents some form of encoded data.
use super::sys; use super::sys;

View File

@ -1,4 +1,4 @@
//! #![allow(missing_docs)]
use keyfork_crossterm::{ use keyfork_crossterm::{
execute, execute,

View File

@ -1,4 +1,4 @@
//! //! Generate entropy of a given size, encoded as hex.
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let bit_size: usize = std::env::args() let bit_size: usize = std::env::args()

View File

@ -1,4 +1,4 @@
//! //! Generate a mnemonic from hex-encoded input.
use keyfork_mnemonic::Mnemonic; use keyfork_mnemonic::Mnemonic;

View File

@ -1,4 +1,4 @@
//! #![allow(missing_docs)]
use std::io::{stdin, stdout}; use std::io::{stdin, stdout};

View File

@ -1,3 +1,9 @@
//! A terminal prompt handler.
//!
//! This prompt handler uses a raw terminal device to read inputs and uses ANSI escape codes to
//! provide formatting for prompts. Because of these reasons, it is not intended to be
//! machine-readable.
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write}, io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},