Source code for stellar_sdk.keypair

import os
from collections.abc import Iterable

import nacl.signing as ed25519
from nacl.exceptions import BadSignatureError as NaclBadSignatureError

from . import xdr as stellar_xdr
from .decorated_signature import DecoratedSignature
from .exceptions import BadSignatureError, MissingEd25519SecretSeedError
from .sep.mnemonic import Language, StellarMnemonic
from .strkey import StrKey
from .utils import sha256

__all__ = ["Keypair"]


[docs] class Keypair: """The :class:`Keypair` object, which represents a signing and verifying key for use with the Stellar network. Instead of instantiating the class directly, we recommend using one of several class methods: * :meth:`Keypair.random` * :meth:`Keypair.from_secret` * :meth:`Keypair.from_public_key` * :meth:`Keypair.from_mnemonic_phrase` * :meth:`Keypair.from_shamir_mnemonic_phrases` Learn how to create a key through our documentation: `Generate Keypair <https://stellar-sdk.readthedocs.io/en/latest/generate_keypair.html>`__. :param verify_key: The verifying (public) Ed25519 key in the keypair. :param signing_key: The signing (private) Ed25519 key in the keypair. """ def __init__( self, verify_key: ed25519.VerifyKey, signing_key: ed25519.SigningKey | None = None, ) -> None: self.verify_key: ed25519.VerifyKey = verify_key self.signing_key: ed25519.SigningKey | None = signing_key
[docs] @classmethod def random(cls) -> "Keypair": """Generate a :class:`Keypair` object from a randomly generated seed. :return: A new :class:`Keypair` object derived by the randomly seed. """ seed = os.urandom(32) return cls.from_raw_ed25519_seed(seed)
[docs] @classmethod def from_secret(cls, secret: str) -> "Keypair": """Generate a :class:`Keypair` object from a secret key. :param secret: secret key (ex. ``"SB2LHKBL24ITV2Y346BU46XPEL45BDAFOOJLZ6SESCJZ6V5JMP7D6G5X"``) :return: A new :class:`Keypair` object derived by the secret. :raise: :exc:`Ed25519SecretSeedInvalidError <stellar_sdk.exceptions.Ed25519SecretSeedInvalidError>`: if `secret` is not a valid ed25519 secret seed. """ raw_secret = StrKey.decode_ed25519_secret_seed(secret) return cls.from_raw_ed25519_seed(raw_secret)
[docs] @classmethod def from_public_key(cls, public_key: str) -> "Keypair": """Generate a :class:`Keypair` object from a public key. :param public_key: public key (ex. ``"GATPGGOIE6VWADVKD3ER3IFO2IH6DTOA5G535ITB3TT66FZFSIZEAU2B"``) :return: A new :class:`Keypair` object derived by the public key. :raise: :exc:`Ed25519PublicKeyInvalidError <stellar_sdk.exceptions.Ed25519PublicKeyInvalidError>`: if `public_key` is not a valid ed25519 public key. """ key = StrKey.decode_ed25519_public_key(public_key) return cls.from_raw_ed25519_public_key(key)
[docs] @classmethod def from_raw_ed25519_seed(cls, raw_seed: bytes) -> "Keypair": """Generate a :class:`Keypair` object from ed25519 secret key seed raw bytes. :param raw_seed: ed25519 secret key seed raw bytes :return: A new :class:`Keypair` object derived by the ed25519 secret key seed raw bytes """ signing_key = ed25519.SigningKey(raw_seed) verify_key = signing_key.verify_key return cls(verify_key, signing_key)
[docs] @classmethod def from_raw_ed25519_public_key(cls, raw_public_key: bytes) -> "Keypair": """Generate a :class:`Keypair` object from ed25519 public key raw bytes. :param raw_public_key: ed25519 public key raw bytes :return: A new :class:`Keypair` object derived by the ed25519 public key raw bytes """ verify_key = ed25519.VerifyKey(raw_public_key) return cls(verify_key)
@property def secret(self) -> str: """Returns secret key associated with this :class:`Keypair` object :return: secret key :raise: :exc:`MissingEd25519SecretSeedError <stellar_sdk.exceptions.MissingEd25519SecretSeedError>` The :class:`Keypair` does not contain secret seed """ if not self.signing_key: raise MissingEd25519SecretSeedError( "The keypair does not contain secret seed. Use Keypair.from_secret, " "Keypair.random or Keypair.from_mnemonic_phrase to create a new keypair with a secret seed." ) # return None is a better approach return StrKey.encode_ed25519_secret_seed(self.raw_secret_key()) @secret.setter def secret(self, secret: str) -> None: raise AttributeError( "Please use `Keypair.from_secret` to generate a new Keypair object." ) @property def public_key(self) -> str: """Returns public key associated with this :class:`Keypair` object :return: public key """ return StrKey.encode_ed25519_public_key(self.raw_public_key()) @public_key.setter def public_key(self, public_key: str) -> None: raise AttributeError( "Please use `Keypair.from_public_key` to generate a new Keypair object." )
[docs] def xdr_public_key(self) -> stellar_xdr.PublicKey: """ :return: xdr public key """ return stellar_xdr.PublicKey( stellar_xdr.PublicKeyType.PUBLIC_KEY_TYPE_ED25519, stellar_xdr.Uint256(bytes(self.verify_key)), )
def xdr_account_id(self) -> stellar_xdr.AccountID: return stellar_xdr.AccountID(self.xdr_public_key()) def xdr_muxed_account(self) -> stellar_xdr.MuxedAccount: return stellar_xdr.MuxedAccount( type=stellar_xdr.CryptoKeyType.KEY_TYPE_ED25519, ed25519=stellar_xdr.Uint256(bytes(self.verify_key)), )
[docs] def raw_public_key(self) -> bytes: """Returns raw public key. :return: raw public key """ return bytes(self.verify_key)
[docs] def signature_hint(self) -> bytes: """Returns signature hint associated with this :class:`Keypair` object :return: signature hint """ ed25519_key = self.xdr_account_id().account_id.ed25519 assert ed25519_key is not None signature_hint = bytes(ed25519_key.uint256[-4:]) return signature_hint
[docs] def raw_secret_key(self) -> bytes: """Returns raw secret key. :return: raw secret key """ if self.signing_key is None: raise MissingEd25519SecretSeedError( "The keypair does not contain secret seed. Use Keypair.from_secret, " "Keypair.random or Keypair.from_mnemonic_phrase to create a new keypair with a secret seed." ) return bytes(self.signing_key)
[docs] def can_sign(self) -> bool: """Returns `True` if this :class:`Keypair` object contains secret key and can sign. :return: `True` if this :class:`Keypair` object contains secret key and can sign """ return self.signing_key is not None
[docs] def sign(self, data: bytes) -> bytes: """Sign the provided data with the keypair's private key. :param data: The data to sign. :return: signed bytes :raise: :exc:`MissingEd25519SecretSeedError <stellar_sdk.exceptions.MissingEd25519SecretSeedError>`: if :class:`Keypair` does not contain secret seed. """ if not self.can_sign(): raise MissingEd25519SecretSeedError( "The keypair does not contain secret seed. Use Keypair.from_secret, " "Keypair.random or Keypair.from_mnemonic_phrase to create a new keypair with a secret seed." ) assert self.signing_key is not None return self.signing_key.sign(data).signature
[docs] def verify(self, data: bytes, signature: bytes) -> None: """Verify the provided data and signature match this keypair's public key. :param data: The data that was signed. :param signature: The signature. :raise: :exc:`BadSignatureError <stellar_sdk.exceptions.BadSignatureError>`: if the verification failed and the signature was incorrect. """ try: self.verify_key.verify(data, signature) except (NaclBadSignatureError, ValueError) as e: raise BadSignatureError("Signature verification failed.") from e
[docs] @staticmethod def generate_mnemonic_phrase( language: Language | str = Language.ENGLISH, strength: int = 128 ) -> str: """Generate a mnemonic phrase. :param language: The language of the mnemonic phrase, defaults to english. :param strength: The complexity of the mnemonic, its possible value is ``128``, ``160``, ``192``, ``224`` and ``256``. :return: A mnemonic phrase. """ mnemonic_phrase = StellarMnemonic(language).generate(strength) return mnemonic_phrase
[docs] @classmethod def from_mnemonic_phrase( cls, mnemonic_phrase: str, language: Language | str = Language.ENGLISH, passphrase: str = "", index: int = 0, ) -> "Keypair": """Generate a :class:`Keypair` object via a mnemonic phrase. :param mnemonic_phrase: A unique string used to deterministically generate keypairs. :param language: The language of the mnemonic phrase, defaults to english. :param passphrase: An optional passphrase used as part of the salt during PBKDF2 rounds when generating the seed from the mnemonic. :param index: The index of the keypair generated by the mnemonic. This allows for multiple Keypairs to be derived from the same mnemonic, such as:: >>> from stellar_sdk.keypair import Keypair >>> mnemonic = 'update hello cry airport drive chunk elite boat shaft sea describe number' # Don't use this mnemonic in practice. >>> kp1 = Keypair.from_mnemonic_phrase(mnemonic, index=0) >>> kp2 = Keypair.from_mnemonic_phrase(mnemonic, index=1) >>> kp3 = Keypair.from_mnemonic_phrase(mnemonic, index=2) :return: A new :class:`Keypair` object derived from the mnemonic. """ raw_ed25519_seed = StellarMnemonic(language).to_seed( mnemonic_phrase, passphrase, index ) return cls.from_raw_ed25519_seed(raw_ed25519_seed)
[docs] @staticmethod def generate_shamir_mnemonic_phrases( member_threshold: int, member_count: int, passphrase: str = "", strength: int = 256, ) -> list[str]: """Generate mnemonic phrases using Shamir secret sharing method. A randomly generated secret key is generated and split into `member_count` mnemonic phrases. The secret key can be later reconstructed using any subset of `member_threshold` 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: import shamir_mnemonic # type: ignore[import-untyped] except ModuleNotFoundError as exc: message = "shamir_mnemonic must be installed to use method `generate_shamir_mnemonic_phrases`." raise ModuleNotFoundError(message) from exc # 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=entropy, passphrase=passphrase.encode(), )[0] except shamir_mnemonic.utils.MnemonicError as exc: raise ValueError(exc) from exc return phrases
[docs] @classmethod def from_shamir_mnemonic_phrases( cls, mnemonic_phrases: Iterable[str], passphrase: str = "", index: int = 0, ) -> "Keypair": """Generate a :class:`Keypair` object via a list of mnemonic phrases. :param mnemonic_phrases: A list of unique strings used to deterministically generate a keypair. :param passphrase: An optional passphrase used to decrypt the secret key. :param index: The index of the keypair generated by the mnemonic. This allows for multiple Keypairs to be derived from the same mnemonic. :return: A new :class:`Keypair` object derived from the mnemonic phrases. """ try: import shamir_mnemonic # noqa: F401 except ModuleNotFoundError as exc: message = "shamir_mnemonic must be installed to use method `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)
[docs] def sign_decorated(self, data: bytes) -> DecoratedSignature: """Sign the provided data with the keypair's private key and returns DecoratedSignature. :param data: signed bytes :return: sign decorated """ hint = self.signature_hint() signature = self.sign(data) return DecoratedSignature(hint, signature)
[docs] def sign_payload_decorated(self, data: bytes) -> DecoratedSignature: """Returns the decorated signature hint for a signed payload signer. The signature hint of an ed25519 signed payload signer is the last 4 bytes of the ed25519 public key XORed with last 4 bytes of the payload. If the payload has a length less than 4 bytes, then 1 to 4 zero bytes are appended to the payload such that it has a length of 4 bytes, for calculating the hint. :param data: data to both sign and treat as the payload :return: sign decorated """ key_hint = self.signature_hint() signature = self.sign(data) data_len = len(data) payload_hint = bytearray(4) payload_hint[: data_len if data_len < 4 else 4] = data[-4:] hint = bytes(map(lambda x, y: x ^ y, key_hint, payload_hint)) return DecoratedSignature(hint, signature)
@staticmethod def _calculate_message_hash(message: str | bytes) -> bytes: """Calculate the hash of a message according to `SEP-53 <https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md>`__. :param message: The message to hash, as a string or bytes. :return: The SHA-256 hash of the prefixed message. """ message_prefix = b"Stellar Signed Message:\n" if isinstance(message, str): message = message.encode("utf-8") signed_message_base = message_prefix + message return sha256(signed_message_base)
[docs] def sign_message(self, message: str | bytes) -> bytes: """Sign a message according to `SEP-53 <https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md>`__. :param message: The message to sign, as a string or bytes. :return: The signature bytes. """ message_hash = self._calculate_message_hash(message) return self.sign(message_hash)
[docs] def verify_message(self, message: str | bytes, signature: bytes) -> None: """Verify a `SEP-53 <https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md>`__ signed message. :param message: The original message, as a string or bytes. :param signature: The signature to verify. :raise: :exc:`BadSignatureError <stellar_sdk.exceptions.BadSignatureError>`: if the verification failed and the signature was incorrect. """ message_hash = self._calculate_message_hash(message) return self.verify(message_hash, signature)
def __hash__(self): return hash((self.verify_key, self.signing_key)) def __eq__(self, other: object) -> bool: if not isinstance(other, self.__class__): return NotImplemented return ( self.verify_key == other.verify_key and self.signing_key == other.signing_key ) def __repr__(self): return f"<Keypair [public_key={self.public_key}, private_key_exists={self.can_sign()}]>"