deterministic_keygen/
lib.rs

1use anyhow::Error;
2use bip39::{Language, Mnemonic, MnemonicType};
3use blake3;
4use pyo3::exceptions::{PyValueError, PyRuntimeError};
5use pyo3::prelude::*;
6use pyo3::types::PyBytes;
7use rand_chacha::ChaCha20Rng;
8use rand_chacha::rand_core::SeedableRng;
9use rsa::RsaPrivateKey;
10use rsa::pkcs8::{EncodePrivateKey, LineEnding};
11use std::str;
12
13// As per the Blake3 docs <https://docs.rs/blake3/latest/blake3/fn.derive_key.html>:
14// "The context string should be hardcoded, globally unique, and application-specific.
15// A good default format for the context string is '[application] [commit timestamp]
16// [purpose]':"
17const RSA_CONTEXT: &str = "deterministic-keygen Wed 07 Feb 2024 11:50:00 AM EST RSA v1";
18
19/// Generate a new BIP-39 mnemonic phrase.
20#[pyfunction]
21pub fn generate_phrase() -> String {
22    Mnemonic::new(MnemonicType::Words12, Language::English)
23        .phrase()
24        .to_string()
25}
26
27#[test]
28fn test_generate_phrase_returns_12_words() {
29    let phrase = generate_phrase();
30    assert_eq!(phrase.split_whitespace().count(), 12);
31}
32
33/// Convert a BIP-39 mnemonic phrase to its corresponding entropy.
34pub fn phrase_to_entropy(phrase: &str) -> Result<Vec<u8>, Error> {
35    let mnemonic = Mnemonic::from_phrase(phrase, Language::English)?;
36    let entropy: &[u8] = mnemonic.entropy();
37    Ok(entropy.to_vec())
38}
39
40#[test]
41fn test_phrase_to_entropy() {
42    let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
43    let entropy = phrase_to_entropy(phrase).unwrap();
44    let expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
45    assert_eq!(entropy, expected);
46}
47
48/// Derive an RSA key from a given vector of unsigned 8-bit integers.
49pub fn derive_rsa_key(entropy: &Vec<u8>, bit_size: usize) -> Result<String, Error> {
50    let seed: [u8; 32] = blake3::derive_key(RSA_CONTEXT, &entropy);
51    let mut rng = ChaCha20Rng::from_seed(seed);
52    let priv_key = RsaPrivateKey::new(&mut rng, bit_size)?;
53    let pem = priv_key.to_pkcs8_pem(LineEnding::LF)?;
54    Ok(pem.to_string())
55}
56
57#[test]
58fn test_derive_rsa_key() {
59    let phrase = generate_phrase();
60    let entropy = phrase_to_entropy(&phrase).unwrap();
61    let key1 = derive_rsa_key(&entropy, 512).unwrap();
62    let key2 = derive_rsa_key(&entropy, 512).unwrap();
63    assert_eq!(key1, key2);
64}
65
66/// Derive an RSA key from a given sequence of bytes.
67#[pyfunction]
68#[pyo3(name = "derive_rsa_key")]
69#[pyo3(signature = (entropy, bit_size = 2048))]
70pub fn py_derive_rsa_key(entropy: &PyBytes, bit_size: usize) -> PyResult<String> {
71    match derive_rsa_key(&Vec::from(entropy.as_bytes()), bit_size) {
72        Err(error) => Err(PyRuntimeError::new_err(error.to_string())),
73        Ok(key) => Ok(key),
74    }
75}
76
77/// Derive an RSA key from a given BIP-39 mnemonic phrase.
78#[pyfunction]
79#[pyo3(signature = (phrase, bit_size = 2048))]
80pub fn derive_rsa_key_from_phrase(phrase: &str, bit_size: usize) -> PyResult<String> {
81    let entropy = match phrase_to_entropy(phrase) {
82        Err(error) => return Err(PyValueError::new_err(error.to_string())),
83        Ok(entropy) => entropy,
84    };
85    match derive_rsa_key(&entropy, bit_size) {
86        Err(error) => Err(PyRuntimeError::new_err(error.to_string())),
87        Ok(key) => Ok(key),
88    }
89}
90
91/// Deterministic key-generator.
92#[pymodule]
93fn deterministic_keygen(_py: Python, m: &PyModule) -> PyResult<()> {
94    m.add_function(wrap_pyfunction!(generate_phrase, m)?)?;
95    m.add_function(wrap_pyfunction!(py_derive_rsa_key, m)?)?;
96    m.add_function(wrap_pyfunction!(derive_rsa_key_from_phrase, m)?)?;
97    Ok(())
98}