Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SEP-5 tests for Shamir, more documentation and examples #1015

Merged
merged 10 commits into from
Jan 11, 2025
20 changes: 19 additions & 1 deletion docs/en/generate_keypair.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Vou can also generate a mnemonic phrase and later use it to generate a keypair:
print(f"Public Key: {keypair.public_key}")
print(f"Secret Seed: {keypair.secret}")

Lastly, you can also use the Shamir secret shamir method to split a mnemonic
Lastly, you can also use the Shamir secret sharing method to split a mnemonic
phrase into multiple phrases. In the following example, we need exactly 2
phrases in order to reconstruct the secret:

Expand All @@ -76,3 +76,21 @@ phrases in order to reconstruct the secret:
keypair = Keypair.from_shamir_mnemonic_phrases(mnemonic_phrases[:2]) # any combinations
print(f"Public Key: {keypair.public_key}")
print(f"Secret Seed: {keypair.secret}")

If you want to convert an existing mnemonic phrase to Shamir, you need to get
overcat marked this conversation as resolved.
Show resolved Hide resolved
the corresponding entropy. You can use these lower level functions:

.. code-block:: python
:linenos:

import shamir_mnemonic
from stellar_sdk.sep.mnemonic import StellarMnemonic

seed_raw = StellarMnemonic("english").to_entropy(mnemonic)
mnemonic_phrases = shamir_mnemonic.generate_mnemonics(
group_threshold=1,
groups=[(2, 3)],
master_secret=seed_raw,
passphrase=passphrase.encode(),
)[0]
print(f"Mnemonic phrases: {mnemonic_phrases}")
22 changes: 19 additions & 3 deletions stellar_sdk/keypair.py
overcat marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,10 @@ def from_mnemonic_phrase(

@staticmethod
def generate_shamir_mnemonic_phrases(
member_threshold: int, member_count: int, passphrase: str = ""
member_threshold: int,
member_count: int,
passphrase: str = "",
strength: int = 256,
) -> List[str]:
"""Generate mnemonic phrases using Shamir secret sharing method.

Expand All @@ -270,6 +273,10 @@ def generate_shamir_mnemonic_phrases(
:param member_threshold: Number of members required to reconstruct the secret key.
:param member_count: Number of shares the secret is split into.
:param passphrase: An optional passphrase used to decrypt the secret key.
:param strength: The complexity of the mnemonics in terms of bites, its possible
value is ``128``, ``160``, ``192``, ``224`` and ``256``.
Strengths of ``128`` and ``256`` lead respectively to
shares with 20 and 33 words.
:return: A list of mnemonic phrases.
"""
try:
Expand All @@ -278,12 +285,19 @@ def generate_shamir_mnemonic_phrases(
message = "shamir_mnemonic must be installed to use method `generate_shamir_mnemonic_phrases`."
raise ModuleNotFoundError(message) from exc

secrets = Keypair.random().secret.encode()
overcat marked this conversation as resolved.
Show resolved Hide resolved
# it can be a multiple of 16, one can use a higher entropy if they want
# still for simplicity in the public API we show common values
if strength % 16 != 0:
raise ValueError(
f"Strength should be one of the following (128, 160, 192, 224, 256), but it is not ({strength})."
)

entropy = os.urandom(strength // 8)
try:
phrases = shamir_mnemonic.generate_mnemonics(
group_threshold=1,
groups=[(member_threshold, member_count)],
master_secret=secrets,
master_secret=entropy,
passphrase=passphrase.encode(),
)[0]
except shamir_mnemonic.utils.MnemonicError as exc:
Expand Down Expand Up @@ -314,12 +328,14 @@ def from_shamir_mnemonic_phrases(
raise ModuleNotFoundError(message) from exc

try:
# Shamir -> entropy
main_seed = shamir_mnemonic.combine_mnemonics(
mnemonics=mnemonic_phrases, passphrase=passphrase.encode()
)
except shamir_mnemonic.utils.MnemonicError as exc:
raise ValueError(exc) from exc

# Entropy -> SLIP-10 -> ED25519
derived_seed = StellarMnemonic.derive(seed=main_seed, index=index)
return cls.from_raw_ed25519_seed(derived_seed)

Expand Down
9 changes: 8 additions & 1 deletion stellar_sdk/sep/mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def __init__(self, language: Union[str, Language] = Language.ENGLISH) -> None:
super().__init__(language)

def to_seed(self, mnemonic: str, passphrase: str = "", index: int = 0) -> bytes: # type: ignore[override]
"""Derive an ED25519 key from a mnemonic."""
bip39_seed = self.to_bip39_seed(mnemonic=mnemonic, passphrase=passphrase)
return self.derive(bip39_seed[:64], index)

def to_bip39_seed(self, mnemonic: str, passphrase: str = "") -> bytes:
"""Derive a BIP-39 key from a mnemonic."""
if not self.check(mnemonic):
raise ValueError(
"Invalid mnemonic, please check if the mnemonic is correct, "
Expand All @@ -60,7 +66,7 @@ def to_seed(self, mnemonic: str, passphrase: str = "", index: int = 0) -> bytes:
stretched = hashlib.pbkdf2_hmac(
"sha512", mnemonic_bytes, passphrase_bytes, PBKDF2_ROUNDS
)
return self.derive(stretched[:64], index)
return stretched

def generate(self, strength: int = 128) -> str:
if strength not in (128, 160, 192, 224, 256):
Expand All @@ -71,6 +77,7 @@ def generate(self, strength: int = 128) -> str:

@staticmethod
def derive(seed: bytes, index: int) -> bytes:
"""Derive an ED25519 key from a BIP-39 seed."""
# References https://github.com/satoshilabs/slips/blob/master/slip-0010.md
master_hmac = hmac.new(StellarMnemonic.SEED_MODIFIER, digestmod=hashlib.sha512)
master_hmac.update(seed)
Expand Down
Loading
Loading