use cotton::prelude::*;
use rand_core::{OsRng, RngCore};
pub use crypto_box::{SecretKey, PublicKey, generate_nonce};
use crypto_box::{Box as CryptoBox, aead::Aead};
use ed25519_dalek::{Signer, Verifier, Signature, SIGNATURE_LENGTH};
pub use ed25519_dalek::{PublicKey as SignPublicKey, SecretKey as SignSecretKey, Keypair as SignKeypair};
use crate::project_dir;
const NONCE_SIZE: usize = 24;
const KEY_SIZE: usize = 32;
pub const DEFAULT_BASE_STATION_PUBLIC_KEY_FILE: &str = "cbradio-base-station.pub";
pub const DEFAULT_BASE_STATION_SECRET_KEY_FILE: &str = "cbradio-base-station.key";
pub const DEFAULT_NETWORK_PUBLIC_KEY_FILE: &str = "cbradio-network.pub";
pub const DEFAULT_NETWORK_SECRET_KEY_FILE: &str = "cbradio-network.key";
pub fn encode(value: &[u8]) -> String {
base64::encode(value)
}
pub fn decode(value: &str) -> PResult<Vec<u8>> {
base64::decode(value).problem_while("decoding a base64 string")
}
pub fn default_base_staion_public_key_file() -> PathBuf {
project_dir().config_dir().join(DEFAULT_BASE_STATION_PUBLIC_KEY_FILE)
}
pub fn default_base_staion_secret_key_file() -> PathBuf {
project_dir().config_dir().join(DEFAULT_BASE_STATION_SECRET_KEY_FILE)
}
pub fn default_network_public_key_file() -> PathBuf {
project_dir().config_dir().join(DEFAULT_NETWORK_PUBLIC_KEY_FILE)
}
pub fn default_network_secret_key_file() -> PathBuf {
project_dir().config_dir().join(DEFAULT_NETWORK_SECRET_KEY_FILE)
}
pub fn load_secret_key(path: &Path) -> PResult<SecretKey> {
in_context_of("loading Curve25519 secret key form a file", || {
let key = read_to_string(path).problem_while("reading key file")
.and_then(|s| decode(s.trim()))?;
let key: [u8; 32] = key.try_into().ok().ok_or_problem("bad key length")?;
Ok(SecretKey::from(key))
})
}
pub fn load_public_key(path: &Path) -> PResult<PublicKey> {
in_context_of("loading Curve25519 public key form a file", || {
let key = read_to_string(path).problem_while("reading key file")
.and_then(|s| decode(s.trim()))?;
let key: [u8; 32] = key.try_into().ok().ok_or_problem("bad key length")?;
Ok(PublicKey::from(key))
})
}
pub fn load_signing_secret_key(path: &Path) -> PResult<SignSecretKey> {
in_context_of("loading Ed25519 secret key form a file", || {
let key = read_to_string(path).problem_while("reading key file")
.and_then(|s| decode(s.trim()))?;
Ok(SignSecretKey::from_bytes(&key)?)
})
}
pub fn load_signing_public_key(path: &Path) -> PResult<SignPublicKey> {
in_context_of("loading Ed25519 public key form a file", || {
let key = read_to_string(path).problem_while("reading key file")
.and_then(|s| decode(s.trim()))?;
Ok(SignPublicKey::from_bytes(&key)?)
})
}
pub fn generate_encryption_key() -> SecretKey {
SecretKey::generate(&mut OsRng)
}
pub fn generate_signing_key() -> SignSecretKey {
let mut bytes = [0u8; KEY_SIZE];
OsRng.fill_bytes(&mut bytes);
SignSecretKey::from_bytes(&bytes).unwrap()
}
pub fn make_keypair(secret_key: SignSecretKey) -> SignKeypair {
SignKeypair {
public: (&secret_key).into(),
secret: secret_key,
}
}
pub type Challenge = [u8; NONCE_SIZE];
pub fn generate_challenge() -> Challenge {
generate_nonce(&mut OsRng).into()
}
pub fn make_box(cleartext: Vec<u8>, recipient_public_key: &PublicKey, ephemeral_secret_key: &SecretKey) -> PResult<Vec<u8>> {
in_context_of("making sealed box", || {
let ephemeral_public_key = ephemeral_secret_key.public_key();
let cbox = CryptoBox::new(recipient_public_key, ephemeral_secret_key);
let nonce = generate_nonce(&mut OsRng);
let mut ciphertext = cbox.encrypt(&nonce, cleartext.as_slice()).ok().ok_or_problem("Failed to encrypt")?;
ciphertext.extend_from_slice(ephemeral_public_key.as_bytes());
ciphertext.extend_from_slice(&nonce);
Ok(ciphertext)
})
}
pub fn open_box(ciphertext: &[u8], secret_key: &SecretKey) -> PResult<(PublicKey, Vec<u8>)> {
in_context_of("opening sealed box", || {
if ciphertext.len() <= KEY_SIZE + NONCE_SIZE {
return problem!("Ciphertext too short")
}
let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - NONCE_SIZE);
let (ciphertext, ephemeral_public_key) = ciphertext.split_at(ciphertext.len() - KEY_SIZE);
let ephemeral_public_key: [u8; KEY_SIZE] = ephemeral_public_key[..].try_into().unwrap();
let ephemeral_public_key = PublicKey::from(ephemeral_public_key);
let nonce: [u8; NONCE_SIZE] = nonce[..].try_into().unwrap();
let secret_box = CryptoBox::new(&ephemeral_public_key, &secret_key);
let plaintext = secret_box.decrypt(&nonce.into(), ciphertext).ok().ok_or_problem("Failed to decrypt")?;
Ok((ephemeral_public_key, plaintext))
})
}
pub fn make_signed_box(cleartext: Vec<u8>, signing_keypair: &SignKeypair, recipient_public_key: &PublicKey, ephemeral_secret_key: &SecretKey) -> PResult<Vec<u8>> {
let mut ciphertext = make_box(cleartext, recipient_public_key, ephemeral_secret_key)?;
let signature = signing_keypair.sign(&ciphertext);
ciphertext.extend_from_slice(&signature.to_bytes());
Ok(ciphertext)
}
pub fn open_signed_box(ciphertext: &[u8], signing_public_key: &SignPublicKey, secret_key: &SecretKey) -> PResult<(PublicKey, Vec<u8>)> {
let ciphertext = in_context_of("signed sealed box verification", || {
if ciphertext.len() <= SIGNATURE_LENGTH {
return problem!("Ciphertext too short")
}
let (ciphertext, signature) = ciphertext.split_at(ciphertext.len() - SIGNATURE_LENGTH);
let signature = Signature::try_from(signature)?;
signing_public_key.verify(ciphertext, &signature)?;
Ok(ciphertext)
})?;
open_box(ciphertext, secret_key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crypto_box() {
let network = generate_encryption_key();
let network_pub = network.public_key();
let session = generate_encryption_key();
let session_pub = session.public_key();
let station_to_agent = make_box(b"station to agent".to_vec(), &network_pub, &session).unwrap();
assert_eq!(&open_box(&station_to_agent, &network).unwrap().1, b"station to agent");
let agent_to_station = make_box(b"agent to station".to_vec(), &session_pub, &network).unwrap();
assert_eq!(&open_box(&agent_to_station, &session).unwrap().1, b"agent to station");
}
#[test]
fn test_signed_crypto_box() {
let network = generate_encryption_key();
let network_pub = network.public_key();
let session = generate_encryption_key();
let station = generate_signing_key();
let station_pub: SignPublicKey = (&station).into();
let station_keypair = make_keypair(station);
let station_to_agent = make_signed_box(b"station to agent".to_vec(), &station_keypair, &network_pub, &session).unwrap();
assert_eq!(&open_signed_box(&station_to_agent, &station_pub, &network).unwrap().1, b"station to agent");
}
}