Repository: crwood/deterministic-keygen (language: python)

Issue:
Python bindings currently require the use of English BIP39 mnemonics
Presently, the Python bindings for this library expose a higher-level/convenience `derive_rsa_key_from_phrase` function that takes a 12-word English BIP39 mnemonic phrase and returns a derived RSA key. It does _not_ expose the lower-level Rust `derive_rsa_key` function (which takes bytes). Arguably, this is unnecessarily restrictive, as it prevents the use of other wordlists or derivation schemes in Python applications (and is a restriction which is not present for Rust applications -- which can call `derive_rsa_key` directly); generally speaking, it would be preferable if the choice of wordlist -- including whether to use a wordlist at all -- were ultimately left up to the caller/user, and not enforced as a requirement by this library.

In order to facilitate the use of other wordlists for Python applications (as well as to have APIs that are more consistent between Rust and Python consumers), the same lower-level (`derive_rsa_key`) function should be exposed via pyo3 bindings.





Discussion / hints:

Fix patch:
diff --git a/deterministic_keygen.pyi b/deterministic_keygen.pyi
index b344fc5..e784ca0 100644
--- a/deterministic_keygen.pyi
+++ b/deterministic_keygen.pyi
@@ -1,2 +1,3 @@
 def generate_phrase() -> str: ...
+def derive_rsa_key(entropy: bytes, bit_size: int = 2048) -> str: ...
 def derive_rsa_key_from_phrase(phrase: str, bit_size: int = 2048) -> str: ...
diff --git a/src/lib.rs b/src/lib.rs
index 8881f10..c6acc2c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@ use bip39::{Language, Mnemonic, MnemonicType};
 use blake3;
 use pyo3::exceptions::{PyValueError, PyRuntimeError};
 use pyo3::prelude::*;
+use pyo3::types::PyBytes;
 use rand_chacha::ChaCha20Rng;
 use rand_chacha::rand_core::SeedableRng;
 use rsa::RsaPrivateKey;
@@ -45,7 +46,6 @@ fn test_phrase_to_entropy() {
 }
 
 /// Derive an RSA key from a given vector of unsigned 8-bit integers.
-// #[pyfunction]
 pub fn derive_rsa_key(entropy: &Vec<u8>, bit_size: usize) -> Result<String, Error> {
     let seed: [u8; 32] = blake3::derive_key(RSA_CONTEXT, &entropy);
     let mut rng = ChaCha20Rng::from_seed(seed);
@@ -63,6 +63,17 @@ fn test_derive_rsa_key() {
     assert_eq!(key1, key2);
 }
 
+/// Derive an RSA key from a given sequence of bytes.
+#[pyfunction]
+#[pyo3(name = "derive_rsa_key")]
+#[pyo3(signature = (entropy, bit_size = 2048))]
+pub fn py_derive_rsa_key(entropy: &PyBytes, bit_size: usize) -> PyResult<String> {
+    match derive_rsa_key(&Vec::from(entropy.as_bytes()), bit_size) {
+        Err(error) => Err(PyRuntimeError::new_err(error.to_string())),
+        Ok(key) => Ok(key),
+    }
+}
+
 /// Derive an RSA key from a given BIP-39 mnemonic phrase.
 #[pyfunction]
 #[pyo3(signature = (phrase, bit_size = 2048))]
@@ -81,6 +92,7 @@ pub fn derive_rsa_key_from_phrase(phrase: &str, bit_size: usize) -> PyResult<Str
 #[pymodule]
 fn deterministic_keygen(_py: Python, m: &PyModule) -> PyResult<()> {
     m.add_function(wrap_pyfunction!(generate_phrase, m)?)?;
+    m.add_function(wrap_pyfunction!(py_derive_rsa_key, m)?)?;
     m.add_function(wrap_pyfunction!(derive_rsa_key_from_phrase, m)?)?;
     Ok(())
 }


Test patch:
diff --git a/tests/test_deterministic_keygen.py b/tests/test_deterministic_keygen.py
index 267fa49..c756709 100644
--- a/tests/test_deterministic_keygen.py
+++ b/tests/test_deterministic_keygen.py
@@ -1,6 +1,10 @@
 import pytest
 
-from deterministic_keygen import generate_phrase, derive_rsa_key_from_phrase
+from deterministic_keygen import (
+    generate_phrase,
+    derive_rsa_key,
+    derive_rsa_key_from_phrase,
+)
 
 
 def test_generate_phrase_returns_12_words() -> None:
@@ -8,6 +12,13 @@ def test_generate_phrase_returns_12_words() -> None:
     assert len(phrase.split()) == 12
 
 
+def test_derive_rsa_key():
+    entropy = b'\x00' * 32
+    key1 = derive_rsa_key(entropy)
+    key2 = derive_rsa_key(entropy)
+    assert key1 == key2
+
+
 @pytest.mark.parametrize("phrase, msg", [
     ("abandon", "invalid number of words in phrase: 1"),
     ("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon notaword", "invalid word in phrase"),