cbradio 0.1.0

System orchestration based on Redis
Documentation
// SPDX-FileCopyrightText: 2021 Jakub Pastuszek <jpastuszek@protonmail.com>
//
// SPDX-License-Identifier: GPL-3.0-or-later
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)?)
    })
}

/// Generates Curve25519 (Montgomery) key for encryption/decryption of crypto box.
pub fn generate_encryption_key() -> SecretKey {
    SecretKey::generate(&mut OsRng)
}

/// Generated Ed25519 (Edwards) key for signature and verification.
pub fn generate_signing_key() -> SignSecretKey {
    //Note: SignSecretKey::generate does not compile...
    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()
}

/// Makes a sealed box encrypted with ephemeral key and recipient public key and random nonce.
///
/// Returns: box(cleartext, recipient_pk, ephemeral_sk, nonce=rand()) ‖ ephemeral_pk ‖ nonce
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);
        // generating nonce every time since agents will crate multiple responses with the same key
        // pairs
        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))
    })
}

/// Makes signed sealed box with given signing_keypair.
///
/// Returns: make_box(..) ‖ sign(make_box(..), signing_keypair)
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");
    }
}