import base64
import binascii
import struct
from enum import Enum
from xdrlib import Packer, Unpacker
from . import xdr as stellar_xdr
from .exceptions import (
Ed25519PublicKeyInvalidError,
Ed25519SecretSeedInvalidError,
MuxedEd25519AccountInvalidError,
TypeError,
ValueError,
)
from .type_checked import type_checked
__all__ = ["StrKey"]
class _VersionByte(Enum):
ED25519_PUBLIC_KEY = binascii.a2b_hex("30") # G 48 6 << 3
ED25519_SECRET_SEED = binascii.a2b_hex("90") # S 144 18 << 3
PRE_AUTH_TX = binascii.a2b_hex("98") # T 152 19 << 3
SHA256_HASH = binascii.a2b_hex("b8") # X 184 23 << 3
MUXED_ACCOUNT = binascii.a2b_hex("60") # M 96 12 << 3
[docs]@type_checked
class StrKey:
"""StrKey is a helper class that allows encoding and decoding strkey."""
[docs] @staticmethod
def encode_ed25519_public_key(data: bytes) -> str:
"""Encodes data to encoded ed25519 public key strkey.
:param data: data to encode
:return: encoded ed25519 public key strkey
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
return _encode_check(_VersionByte.ED25519_PUBLIC_KEY, data)
[docs] @staticmethod
def decode_ed25519_public_key(data: str) -> bytes:
"""Decodes encoded ed25519 public key strkey to raw data.
:param data: encoded ed25519 public key strkey
:return: raw bytes
:raises:
:exc:`Ed25519PublicKeyInvalidError <stellar_sdk.exceptions.Ed25519PublicKeyInvalidError>`
"""
try:
return _decode_check(_VersionByte.ED25519_PUBLIC_KEY, data)
except Exception:
raise Ed25519PublicKeyInvalidError(f"Invalid Ed25519 Public Key: {data}")
[docs] @staticmethod
def is_valid_ed25519_public_key(public_key: str) -> bool:
"""Returns ``True`` if the given `seed` is a valid ed25519 public key strkey.
:param public_key: encoded ed25519 public key strkey
:return: ``True`` if the given key is valid
"""
return _is_valid(_VersionByte.ED25519_PUBLIC_KEY, public_key)
[docs] @staticmethod
def encode_ed25519_secret_seed(data: bytes) -> str:
"""Encodes data to encoded ed25519 secret seed strkey.
:param data: data to encode
:return: encoded ed25519 secret seed strkey
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
return _encode_check(_VersionByte.ED25519_SECRET_SEED, data)
[docs] @staticmethod
def decode_ed25519_secret_seed(data: str) -> bytes:
"""Decodes encoded ed25519 secret seed strkey to raw data.
:param data: encoded ed25519 secret seed strkey
:return: raw bytes
:raises:
:exc:`Ed25519SecretSeedInvalidError <stellar_sdk.exceptions.Ed25519SecretSeedInvalidError>`
"""
try:
return _decode_check(_VersionByte.ED25519_SECRET_SEED, data)
except Exception:
raise Ed25519SecretSeedInvalidError(f"Invalid Ed25519 Secret Seed: {data}")
[docs] @staticmethod
def is_valid_ed25519_secret_seed(seed: str) -> bool:
"""Returns ``True`` if the given `seed` is a valid ed25519 secret seed strkey.
:param seed: encoded ed25519 secret seed strkey
:return: ``True`` if the given key is valid
"""
return _is_valid(_VersionByte.ED25519_SECRET_SEED, seed)
[docs] @staticmethod
def encode_pre_auth_tx(data: bytes) -> str:
"""Encodes data to encoded pre auth tx strkey.
:param data: data to encode
:return: encoded pre auth tx strkey
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
return _encode_check(_VersionByte.PRE_AUTH_TX, data)
[docs] @staticmethod
def decode_pre_auth_tx(data: str) -> bytes:
"""Decodes encoded pre auth tx strkey to raw data.
:param data: encoded pre auth tx strkey
:return: raw bytes
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
try:
return _decode_check(_VersionByte.PRE_AUTH_TX, data)
except Exception as e:
raise ValueError(f"Invalid Pre Auth Tx Key: {data}") from e
[docs] @staticmethod
def is_valid_pre_auth_tx(pre_auth_tx: str) -> bool:
"""Returns ``True`` if the given `pre_auth_tx` is a valid encoded pre auth tx strkey.
:param pre_auth_tx: encoded pre auth tx strkey
:return: ``True`` if the given key is valid
"""
return _is_valid(_VersionByte.PRE_AUTH_TX, pre_auth_tx)
[docs] @staticmethod
def encode_sha256_hash(data: bytes) -> str:
"""Encodes data to encoded sha256 hash strkey.
:param data: data to encode
:return: encoded sha256 hash strkey
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
return _encode_check(_VersionByte.SHA256_HASH, data)
[docs] @staticmethod
def decode_sha256_hash(data: str) -> bytes:
"""Decodes encoded sha256 hash strkey to raw data.
:param data: encoded sha256 hash strkey
:return: raw bytes
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
try:
return _decode_check(_VersionByte.SHA256_HASH, data)
except Exception as e:
raise ValueError(f"Invalid sha256 Hash Key: {data}") from e
[docs] @staticmethod
def is_valid_sha256_hash(sha256_hash: str) -> bool:
"""Returns ``True`` if the given `sha256_hash` is a valid encoded sha256 hash(HashX) strkey.
:param sha256_hash: encoded sha256 hash(HashX) strkey
:return: ``True`` if the given key is valid
"""
return _is_valid(_VersionByte.SHA256_HASH, sha256_hash)
[docs] @staticmethod
def encode_muxed_account(data: stellar_xdr.MuxedAccount) -> str:
"""Encodes data to encoded muxed account strkey.
:param data: data to encode
:return: encoded muxed account strkey
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
if data.type == stellar_xdr.CryptoKeyType.KEY_TYPE_ED25519:
assert data.ed25519 is not None
return StrKey.encode_ed25519_public_key(data.ed25519.uint256)
assert data.med25519 is not None
packer = Packer()
data.med25519.ed25519.pack(packer)
data.med25519.id.pack(packer)
return _encode_check(_VersionByte.MUXED_ACCOUNT, packer.get_buffer())
[docs] @staticmethod
def decode_muxed_account(data: str) -> stellar_xdr.MuxedAccount:
"""Decodes encoded muxed account strkey to raw data.
:param data: encoded muxed account strkey
:return: raw bytes
:raises:
:exc:`ValueError <stellar_sdk.exceptions.ValueError>`
"""
data_length = len(data)
if data_length == 56:
muxed = stellar_xdr.MuxedAccount(
type=stellar_xdr.CryptoKeyType.KEY_TYPE_ED25519,
ed25519=stellar_xdr.Uint256(StrKey.decode_ed25519_public_key(data)),
)
elif data_length == 69:
# let's optimize it in v3.
try:
xdr_bytes = _decode_check(_VersionByte.MUXED_ACCOUNT, data)
except Exception:
raise MuxedEd25519AccountInvalidError(
"Invalid Muxed Account: {}".format(data)
)
unpacker = Unpacker(xdr_bytes)
ed25519 = stellar_xdr.Uint256.unpack(unpacker)
id = stellar_xdr.Uint64.unpack(unpacker)
med25519 = stellar_xdr.MuxedAccountMed25519(
id=id,
ed25519=ed25519,
)
muxed = stellar_xdr.MuxedAccount(
type=stellar_xdr.CryptoKeyType.KEY_TYPE_MUXED_ED25519, med25519=med25519
)
else:
raise ValueError("Invalid encoded string, this is not a valid account.")
return muxed
@type_checked
def _decode_check(version_byte: _VersionByte, encoded: str) -> bytes:
encoded_data = encoded.encode("ascii")
encoded_data = encoded_data + b"=" * ((4 - len(encoded_data) % 4) % 4)
try:
decoded_data = base64.b32decode(encoded_data)
except binascii.Error:
raise ValueError("Incorrect padding.")
if encoded_data != base64.b32encode(decoded_data): # Is that even possible?
raise ValueError("Invalid encoded bytes.")
version_byte_in_data = decoded_data[0:1]
payload = decoded_data[0:-2]
data = decoded_data[1:-2]
checksum = decoded_data[-2:]
if version_byte.value != version_byte_in_data:
raise ValueError(
f"Invalid version byte. Expected {version_byte.value!r}, got {version_byte_in_data!r}"
)
expected_checksum = _calculate_checksum(payload)
if expected_checksum != checksum:
raise ValueError("Invalid checksum")
return data
@type_checked
def _encode_check(version_byte: _VersionByte, data: bytes) -> str:
payload = version_byte.value + data
crc = _calculate_checksum(payload)
return base64.b32encode(payload + crc).decode("utf-8").rstrip("=")
@type_checked
def _is_valid(version_byte: _VersionByte, encoded: str) -> bool:
if encoded and len(encoded) != 56:
return False
try:
_decode_check(version_byte, encoded)
except (ValueError, TypeError):
return False
return True
@type_checked
def _calculate_checksum(payload: bytes) -> bytes:
# memo note: https://gist.github.com/manran/a8357808ef71415d266dc64f0079f298
# This code calculates CRC16-XModem checksum of payload
checksum = binascii.crc_hqx(payload, 0)
# Ensure that the checksum is in LSB order.
return struct.pack("<H", checksum)