summary refs log tree commit diff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs198
1 files changed, 198 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..5e2affe
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,198 @@
+use aho_corasick::{AhoCorasick, MatchKind};
+use crc::{CRC_4_G_704, Crc};
+use ed25519_dalek::{
+    SecretKey, Signature, SigningKey, VerifyingKey, ed25519::signature::SignerMut,
+};
+use include_lines::static_include_lines;
+use rand_chacha::{
+    ChaCha12Rng,
+    rand_core::{RngCore, SeedableRng},
+};
+use serde::{Deserialize, Serialize};
+use sha3::{Digest, Sha3_256};
+
+const CHECKSUM: Crc<u8> = Crc::<u8>::new(&CRC_4_G_704);
+static_include_lines!(DICTIONARY, "src/orchard-street-medium.txt");
+
+/// A signature verifying an arbitrary message
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Certificate {
+    pub key: VerifyingKey,
+    pub sig: Signature,
+}
+
+impl Certificate {
+    /// Verify a message against this certificate
+    pub fn verify(&self, message: &[u8]) -> Result<(), ed25519_dalek::SignatureError> {
+        self.key.verify_strict(message, &self.sig)
+    }
+}
+
+/// Errors that may occur during sealed key decoding
+#[derive(Clone, Debug)]
+pub enum DecodeError {
+    WrongLength,
+    CrcMismatch,
+}
+
+/// A secret key, XOR'd with a hashed password as last-resort security
+#[derive(Serialize, Deserialize)]
+pub struct SealedKey([u8; 32]);
+
+impl SealedKey {
+    /// Seal a raw secret key with a given passphrase
+    pub fn seal(mut key: SecretKey, passphrase: impl AsRef<str>) -> Self {
+        // Initialize crypto
+        let mut hasher = Sha3_256::new();
+
+        // Hash passphrase
+        hasher.update(passphrase.as_ref());
+        let hash = hasher.finalize();
+
+        // XOR with passphrase hash
+        for (k, h) in key.iter_mut().zip(hash) {
+            *k ^= h;
+        }
+
+        Self(key)
+    }
+
+    /// Unseal into a raw secret key with a given passphrase
+    ///
+    /// No checks are done on if the passphrase was valid or not.
+    pub fn unseal(&self, passphrase: impl AsRef<str>) -> SecretKey {
+        // Initialize crypto
+        let mut hasher = Sha3_256::new();
+
+        // Hash passphrase
+        hasher.update(passphrase.as_ref());
+        let hash = hasher.finalize();
+
+        // XOR with passphrase hash
+        let mut key = self.0.clone();
+        for (k, h) in key.iter_mut().zip(hash) {
+            *k ^= h;
+        }
+
+        key
+    }
+
+    /// Encode the key into a list of words suitable for writing down and storing securely
+    fn encode(&self) -> [&'static str; 20] {
+        // Compute CRC
+        let checksum = CHECKSUM.checksum(&self.0);
+
+        // Break into 13-bit indices
+        let mut indices = [0usize; 20];
+        for (i, byte) in self.0.iter().enumerate() {
+            let bit = i * 8;
+            let idx = bit / 13;
+            let shift = (bit as isize) % 13 - 4;
+
+            if shift <= 0 {
+                indices[idx] |= (*byte as usize) << -shift;
+            } else {
+                indices[idx] |= (*byte as usize) >> shift;
+                indices[idx + 1] |= ((*byte as usize) << (13 - shift)) & 0x1fff;
+            }
+        }
+        indices[19] |= checksum as usize;
+
+        indices.map(|i| DICTIONARY[i])
+    }
+
+    pub fn decode(input: impl AsRef<str>) -> Result<Self, DecodeError> {
+        // Strip any non-alphabetic characters
+        let alpha = input
+            .as_ref()
+            .chars()
+            .filter(|c| c.is_alphabetic())
+            .collect::<String>();
+
+        // Construct Aho-Corasick automaton
+        // TODO See if this can be done at compile time
+        let ac = AhoCorasick::builder()
+            .match_kind(MatchKind::LeftmostLongest)
+            .ascii_case_insensitive(true)
+            .build(DICTIONARY)
+            .unwrap();
+
+        // Stream decode the input
+        let indices = ac
+            .find_iter(&alpha)
+            .map(|m| m.pattern().as_usize())
+            .collect::<Vec<_>>();
+
+        // Check length
+        if indices.len() != 20 {
+            return Err(DecodeError::WrongLength);
+        }
+
+        unimplemented!();
+    }
+}
+
+/// A public/sealed-secret keypair capable of producing certificates
+#[derive(Serialize, Deserialize)]
+pub struct Keypair {
+    public: VerifyingKey,
+    secret: SealedKey,
+}
+
+impl Keypair {
+    /// Create a new random keypair with a passphrase
+    pub fn new(passphrase: impl AsRef<str>) -> Self {
+        // Initalize crypto
+        let mut rng = ChaCha12Rng::from_os_rng();
+
+        // Generate secret key
+        let mut unsealed = SecretKey::default();
+        rng.fill_bytes(&mut unsealed);
+
+        // Derive public key
+        let public = SigningKey::from_bytes(&unsealed).verifying_key();
+
+        // Seal secret key
+        let secret = SealedKey::seal(unsealed, passphrase);
+
+        Self { public, secret }
+    }
+
+    /// Sign a message with this keypair
+    pub fn sign(&self, passphrase: impl AsRef<str>, message: &[u8]) -> Certificate {
+        // Unseal secret key
+        let unsealed = self.secret.unseal(passphrase);
+
+        // Sign message
+        let mut signer = SigningKey::from_bytes(&unsealed);
+        let signature = signer.sign(message);
+
+        Certificate {
+            key: self.public.clone(),
+            sig: signature,
+        }
+    }
+
+    /// Sign this keypair's public key with itself
+    pub fn self_sign(&self, passphrase: impl AsRef<str>) -> Certificate {
+        self.sign(passphrase, self.public.as_bytes())
+    }
+
+    /// Encode the secret key into a list of words suitable for writing down and storing securely
+    pub fn encode_secret(&self) -> [&'static str; 20] {
+        self.secret.encode()
+    }
+
+    pub fn from_encoded(
+        encoded: impl AsRef<str>,
+        passphrase: impl AsRef<str>,
+    ) -> Result<Self, DecodeError> {
+        let sealed = SealedKey::decode(encoded)?;
+        let unsealed = sealed.unseal(passphrase);
+
+        Ok(Self {
+            public: SigningKey::from_bytes(&unsealed).verifying_key(),
+            secret: sealed,
+        })
+    }
+}