"""
SEP: 0005
Title: Key Derivation Methods for Stellar Accounts
"""
import hashlib
import hmac
import os
import struct
from enum import Enum, unique
from mnemonic import Mnemonic
from mnemonic.mnemonic import PBKDF2_ROUNDS
[docs]
@unique
class Language(Enum):
"""The type of language supported by the mnemonic."""
JAPANESE = "japanese"
FRENCH = "french"
ENGLISH = "english"
SPANISH = "spanish"
ITALIAN = "italian"
KOREAN = "korean"
CHINESE_SIMPLIFIED = "chinese_simplified"
CHINESE_TRADITIONAL = "chinese_traditional"
[docs]
class StellarMnemonic(Mnemonic):
"""Please use :func:`stellar_sdk.keypair.Keypair.generate_mnemonic_phrase`
and :func:`stellar_sdk.keypair.Keypair.from_mnemonic_phrase`
"""
STELLAR_ACCOUNT_PATH_FORMAT = "m/44'/148'/%d'"
FIRST_HARDENED_INDEX = 0x80000000
SEED_MODIFIER = b"ed25519 seed"
def __init__(self, language: str | Language = Language.ENGLISH) -> None:
if isinstance(language, Language):
language = language.value
else:
if language not in {item.value for item in Language}:
raise ValueError("This language is not supported.")
super().__init__(language)
[docs]
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)
[docs]
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, "
"or if the language is set correctly."
)
mnemonic = self.normalize_string(mnemonic)
passphrase = self.normalize_string(passphrase)
passphrase = "mnemonic" + passphrase
mnemonic_bytes = mnemonic.encode("utf-8")
passphrase_bytes = passphrase.encode("utf-8")
stretched = hashlib.pbkdf2_hmac(
"sha512", mnemonic_bytes, passphrase_bytes, PBKDF2_ROUNDS
)
return stretched
[docs]
def generate(self, strength: int = 128) -> str:
if strength not in (128, 160, 192, 224, 256):
raise ValueError(
f"Strength should be one of the following (128, 160, 192, 224, 256), but it is not ({strength})."
)
return self.to_mnemonic(os.urandom(strength // 8))
[docs]
@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)
il = master_hmac.digest()[:32]
ir = master_hmac.digest()[32:]
path = StellarMnemonic.STELLAR_ACCOUNT_PATH_FORMAT % index
for x in path.split("/")[1:]:
data = (
struct.pack("x")
+ il
+ struct.pack(">I", StellarMnemonic.FIRST_HARDENED_INDEX + int(x[:-1]))
)
i = hmac.new(ir, digestmod=hashlib.sha512)
i.update(data)
il = i.digest()[:32]
ir = i.digest()[32:]
return il