libcrypto 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
libcrypto/keys.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ Private and Public Key Classes
3
+
4
+ This module provides classes for handling private and public keys with
5
+ format conversions, WIF support, and cryptographic operations.
6
+ """
7
+ from typing import Union
8
+ from .secp256k1 import (
9
+ private_key_to_public_key, compress_public_key, decompress_public_key
10
+ )
11
+ from .hash import secure_random_bytes
12
+ from .constants import MAX_PRIVATE_KEY
13
+ from .formats import (
14
+ private_key_to_wif, wif_to_private_key, bytes_to_hex, hex_to_bytes,
15
+ int_to_bytes, bytes_to_int, InvalidFormatError
16
+ )
17
+ from .addresses import AddressGenerator
18
+
19
+
20
+ class KeyError(ValueError):
21
+ """Raised when key operations fail."""
22
+ pass
23
+
24
+
25
+ class PrivateKey:
26
+ """
27
+ Represents a secp256k1 private key.
28
+ """
29
+
30
+ def __init__(self, key: Union[bytes, int, str, None] = None, network: str = 'bitcoin'):
31
+ self.network = network
32
+ if key is None:
33
+ self._key_int = self._generate_random_key()
34
+ else:
35
+ self._key_int = self._normalize_key(key)
36
+
37
+ if not (1 <= self._key_int <= MAX_PRIVATE_KEY):
38
+ raise KeyError(f"Private key is out of valid range (1 to N-1).")
39
+
40
+ self._key_bytes = int_to_bytes(self._key_int, 32)
41
+ self._public_key_cache = {}
42
+
43
+ @staticmethod
44
+ def _generate_random_key() -> int:
45
+ """Generate a cryptographically secure random private key."""
46
+ while True:
47
+ key_bytes = secure_random_bytes(32)
48
+ key_int = bytes_to_int(key_bytes)
49
+ if 1 <= key_int <= MAX_PRIVATE_KEY:
50
+ return key_int
51
+
52
+ def _normalize_key(self, key: Union[bytes, int, str]) -> int:
53
+ """Normalize various key formats to an integer."""
54
+ if isinstance(key, int):
55
+ return key
56
+ if isinstance(key, bytes):
57
+ if len(key) != 32:
58
+ raise KeyError(f"Private key bytes must be 32 bytes, got {len(key)}")
59
+ return bytes_to_int(key)
60
+ if isinstance(key, str):
61
+ try:
62
+ # First, try to decode as WIF
63
+ key_bytes, _, _ = wif_to_private_key(key)
64
+ self.network = _[2] # Update network from WIF
65
+ return bytes_to_int(key_bytes)
66
+ except InvalidFormatError:
67
+ # If WIF fails, try to decode as hex
68
+ try:
69
+ if len(key) != 64:
70
+ raise InvalidFormatError("Hex key must be 64 characters.")
71
+ key_bytes = hex_to_bytes(key)
72
+ return bytes_to_int(key_bytes)
73
+ except InvalidFormatError as e:
74
+ raise KeyError(f"Invalid private key format. Not valid WIF or Hex: {e}") from e
75
+ raise KeyError(f"Unsupported key type: {type(key)}")
76
+
77
+ @property
78
+ def hex(self) -> str:
79
+ return bytes_to_hex(self._key_bytes)
80
+
81
+ @property
82
+ def bytes(self) -> bytes:
83
+ return self._key_bytes
84
+
85
+ @property
86
+ def int(self) -> int:
87
+ return self._key_int
88
+
89
+ def to_wif(self, compressed: bool = True) -> str:
90
+ return private_key_to_wif(self._key_bytes, compressed, self.network)
91
+
92
+ def get_public_key(self, compressed: bool = True) -> 'PublicKey':
93
+ """Get the corresponding public key, using a cache for efficiency."""
94
+ if compressed not in self._public_key_cache:
95
+ public_key_bytes = private_key_to_public_key(self._key_int, compressed)
96
+ self._public_key_cache[compressed] = PublicKey(public_key_bytes)
97
+ return self._public_key_cache[compressed]
98
+
99
+ @classmethod
100
+ def generate(cls, network: str = 'bitcoin') -> 'PrivateKey':
101
+ """Generate a new random private key."""
102
+ return cls(None, network)
103
+
104
+ def __repr__(self) -> str:
105
+ return f"PrivateKey(network='{self.network}')"
106
+
107
+
108
+ class PublicKey:
109
+ """
110
+ Represents a secp256k1 public key.
111
+ """
112
+
113
+ def __init__(self, key: Union[bytes, str]):
114
+ if isinstance(key, str):
115
+ key = hex_to_bytes(key)
116
+
117
+ if len(key) not in [33, 65]:
118
+ raise KeyError(f"Invalid public key length: {len(key)}. Must be 33 or 65 bytes.")
119
+
120
+ self._key_bytes = key
121
+ self.compressed = (len(key) == 33)
122
+
123
+ @property
124
+ def hex(self) -> str:
125
+ return bytes_to_hex(self._key_bytes)
126
+
127
+ @property
128
+ def bytes(self) -> bytes:
129
+ return self._key_bytes
130
+
131
+ def get_address(self, address_type: str = 'p2pkh', network: str = 'bitcoin') -> str:
132
+ """
133
+ Generate an address from this public key.
134
+ This is a method, not a property, as it requires arguments.
135
+ """
136
+ return AddressGenerator.from_public_key(self.bytes, address_type, network)
137
+
138
+ def to_compressed(self) -> 'PublicKey':
139
+ if self.compressed:
140
+ return self
141
+ return PublicKey(compress_public_key(self.bytes))
142
+
143
+ def to_uncompressed(self) -> 'PublicKey':
144
+ if not self.compressed:
145
+ return self
146
+ return PublicKey(decompress_public_key(self.bytes))
147
+
148
+ def __repr__(self) -> str:
149
+ return f"PublicKey('{self.hex}', compressed={self.compressed})"
libcrypto/mnemonic.py ADDED
@@ -0,0 +1,156 @@
1
+ """
2
+ BIP39 Mnemonic Phrase Implementation
3
+
4
+ This module provides comprehensive BIP39 mnemonic phrase functionality including:
5
+ - Secure mnemonic generation with proper entropy
6
+ - Mnemonic validation with checksum verification
7
+ - Mnemonic to seed conversion with PBKDF2
8
+ - Conversion between entropy and mnemonic phrases
9
+ """
10
+ from typing import Union
11
+ from .hash import sha256, bip39_pbkdf2, secure_random_bytes
12
+ from .constants import (
13
+ BIP39_WORD_LIST,
14
+ BIP39_ENTROPY_BITS,
15
+ BIP39_CHECKSUM_BITS,
16
+ VALID_MNEMONIC_LENGTHS,
17
+ ERROR_MESSAGES
18
+ )
19
+
20
+
21
+ class InvalidMnemonicError(ValueError):
22
+ """Raised when a mnemonic phrase is invalid."""
23
+ pass
24
+
25
+
26
+ class InvalidEntropyError(ValueError):
27
+ """Raised when entropy is invalid."""
28
+ pass
29
+
30
+
31
+ def _entropy_to_mnemonic_bits(entropy: bytes) -> str:
32
+ """Internal helper to convert entropy to its bit representation with checksum."""
33
+ entropy_bits_len = len(entropy) * 8
34
+ if entropy_bits_len not in BIP39_ENTROPY_BITS.values():
35
+ raise InvalidEntropyError(f"Invalid entropy length: {len(entropy)} bytes")
36
+
37
+ checksum_len = BIP39_CHECKSUM_BITS[entropy_bits_len // 32 * 3]
38
+
39
+ # Calculate checksum
40
+ checksum_hash = sha256(entropy)
41
+ checksum_bits = bin(checksum_hash[0])[2:].zfill(8)[:checksum_len]
42
+
43
+ # Combine entropy and checksum
44
+ entropy_bits = bin(int.from_bytes(entropy, 'big'))[2:].zfill(entropy_bits_len)
45
+
46
+ return entropy_bits + checksum_bits
47
+
48
+
49
+ def entropy_to_mnemonic(entropy: Union[bytes, str]) -> str:
50
+ """
51
+ Convert entropy to a BIP39 mnemonic phrase.
52
+ """
53
+ if isinstance(entropy, str):
54
+ try:
55
+ entropy = bytes.fromhex(entropy)
56
+ except ValueError as e:
57
+ raise InvalidEntropyError(f"Invalid hex string for entropy: {e}") from e
58
+
59
+ total_bits = _entropy_to_mnemonic_bits(entropy)
60
+
61
+ # Split into 11-bit chunks and map to words
62
+ words = []
63
+ for i in range(0, len(total_bits), 11):
64
+ chunk = total_bits[i:i + 11]
65
+ word_index = int(chunk, 2)
66
+ words.append(BIP39_WORD_LIST[word_index])
67
+
68
+ return ' '.join(words)
69
+
70
+
71
+ def mnemonic_to_entropy(mnemonic: str) -> bytes:
72
+ """
73
+ Convert a BIP39 mnemonic phrase back to its original entropy.
74
+ """
75
+ words = mnemonic.strip().split()
76
+ word_count = len(words)
77
+
78
+ if word_count not in VALID_MNEMONIC_LENGTHS:
79
+ raise InvalidMnemonicError(ERROR_MESSAGES['invalid_mnemonic_length'])
80
+
81
+ # Convert words to a bit string
82
+ bit_string = ""
83
+ for word in words:
84
+ try:
85
+ index = BIP39_WORD_LIST.index(word)
86
+ bit_string += bin(index)[2:].zfill(11)
87
+ except ValueError:
88
+ raise InvalidMnemonicError(f"{ERROR_MESSAGES['invalid_mnemonic_word']}: '{word}'")
89
+
90
+ # Split data and checksum
91
+ entropy_len = BIP39_ENTROPY_BITS[word_count]
92
+ checksum_len = BIP39_CHECKSUM_BITS[word_count]
93
+
94
+ entropy_bits = bit_string[:entropy_len]
95
+ checksum_bits = bit_string[entropy_len:]
96
+
97
+ # Convert entropy bits to bytes
98
+ entropy_bytes = int(entropy_bits, 2).to_bytes(entropy_len // 8, 'big')
99
+
100
+ # Verify checksum
101
+ expected_checksum_hash = sha256(entropy_bytes)
102
+ expected_checksum = bin(expected_checksum_hash[0])[2:].zfill(8)[:checksum_len]
103
+
104
+ if checksum_bits != expected_checksum:
105
+ raise InvalidMnemonicError(ERROR_MESSAGES['invalid_mnemonic_checksum'])
106
+
107
+ return entropy_bytes
108
+
109
+
110
+ def generate_mnemonic(word_count: int = 12) -> str:
111
+ """
112
+ Generates a cryptographically secure mnemonic phrase.
113
+ """
114
+ if word_count not in VALID_MNEMONIC_LENGTHS:
115
+ raise ValueError(ERROR_MESSAGES['invalid_mnemonic_length'])
116
+
117
+ entropy_bits = BIP39_ENTROPY_BITS[word_count]
118
+ entropy = secure_random_bytes(entropy_bits // 8)
119
+
120
+ return entropy_to_mnemonic(entropy)
121
+
122
+
123
+ def validate_mnemonic(mnemonic: str) -> bool:
124
+ """
125
+ Validate a BIP39 mnemonic phrase by trying to convert it to entropy.
126
+ """
127
+ try:
128
+ mnemonic_to_entropy(mnemonic)
129
+ return True
130
+ except InvalidMnemonicError:
131
+ return False
132
+
133
+
134
+ def mnemonic_to_seed(mnemonic: str, passphrase: str = "") -> bytes:
135
+ """
136
+ Convert a BIP39 mnemonic phrase to a seed using PBKDF2.
137
+ """
138
+ # NFKD normalization is recommended by BIP39 spec
139
+ import unicodedata
140
+ normalized_mnemonic = unicodedata.normalize('NFKD', mnemonic.strip())
141
+
142
+ if not validate_mnemonic(normalized_mnemonic):
143
+ raise InvalidMnemonicError("Invalid mnemonic phrase provided.")
144
+
145
+ return bip39_pbkdf2(normalized_mnemonic, passphrase)
146
+
147
+
148
+ __all__ = [
149
+ 'generate_mnemonic',
150
+ 'validate_mnemonic',
151
+ 'mnemonic_to_seed',
152
+ 'mnemonic_to_entropy',
153
+ 'entropy_to_mnemonic',
154
+ 'InvalidMnemonicError',
155
+ 'InvalidEntropyError'
156
+ ]
libcrypto/secp256k1.py ADDED
@@ -0,0 +1,117 @@
1
+ """
2
+ Secp256k1 Elliptic Curve Operations (using the 'ecdsa' library)
3
+
4
+ This module provides a robust interface for secp256k1 operations by
5
+ wrapping the 'ecdsa' library, which is a highly stable and focused package
6
+ for Elliptic Curve Digital Signature Algorithm.
7
+
8
+ This implementation replaces the pycryptodome backend to avoid environment
9
+ and installation issues.
10
+ """
11
+ from typing import Tuple
12
+ from ecdsa import SigningKey, VerifyingKey, SECP256k1
13
+ from ecdsa.util import sigencode_string, sigdecode_string
14
+ from .constants import MAX_PRIVATE_KEY
15
+
16
+
17
+ class Secp256k1Error(ValueError):
18
+ """Custom exception for secp256k1 related errors."""
19
+ pass
20
+
21
+
22
+ def private_key_to_public_key(private_key: int, compressed: bool = True) -> bytes:
23
+ """
24
+ Derives a public key from a private key integer using the 'ecdsa' library.
25
+
26
+ Args:
27
+ private_key: The private key as an integer.
28
+ compressed: If True, returns a 33-byte compressed public key.
29
+ If False, returns a 65-byte uncompressed public key.
30
+
31
+ Returns:
32
+ The public key as a byte string.
33
+
34
+ Raises:
35
+ Secp256k1Error: If the private key is out of the valid range.
36
+ """
37
+ if not (1 <= private_key <= MAX_PRIVATE_KEY):
38
+ raise Secp256k1Error("Private key is out of the valid range (1 to N-1).")
39
+
40
+ try:
41
+ # Create a SigningKey object from the private key bytes.
42
+ private_key_bytes = private_key.to_bytes(32, 'big')
43
+ sk = SigningKey.from_string(private_key_bytes, curve=SECP256k1)
44
+
45
+ # Get the corresponding VerifyingKey (public key).
46
+ vk = sk.verifying_key
47
+
48
+ # Return the public key in the requested format.
49
+ if compressed:
50
+ return vk.to_string("compressed")
51
+ else:
52
+ return vk.to_string("uncompressed")
53
+ except Exception as e:
54
+ raise Secp256k1Error(f"Failed to generate public key with ecdsa: {e}") from e
55
+
56
+
57
+ def public_key_to_point_coords(public_key: bytes) -> Tuple[int, int]:
58
+ """
59
+ Converts a public key byte string into its (x, y) integer coordinates.
60
+
61
+ Args:
62
+ public_key: The public key as bytes (compressed or uncompressed).
63
+
64
+ Returns:
65
+ A tuple containing the (x, y) coordinates as integers.
66
+ """
67
+ try:
68
+ vk = VerifyingKey.from_string(public_key, curve=SECP256k1)
69
+ # The public_point attribute holds the x and y coordinates.
70
+ return (vk.pubkey.point.x(), vk.pubkey.point.y())
71
+ except Exception as e:
72
+ raise Secp256k1Error(f"Failed to extract point from public key: {e}") from e
73
+
74
+
75
+ def decompress_public_key(public_key: bytes) -> bytes:
76
+ """
77
+ Converts a public key to its uncompressed format (65 bytes) using 'ecdsa'.
78
+
79
+ Args:
80
+ public_key: The public key in either compressed or uncompressed format.
81
+
82
+ Returns:
83
+ The 65-byte uncompressed public key.
84
+ """
85
+ try:
86
+ # Create a VerifyingKey from the input bytes. It handles both formats.
87
+ vk = VerifyingKey.from_string(public_key, curve=SECP256k1)
88
+ return vk.to_string("uncompressed")
89
+ except Exception as e:
90
+ raise Secp256k1Error(f"Failed to decompress public key: {e}") from e
91
+
92
+
93
+ def compress_public_key(public_key: bytes) -> bytes:
94
+ """
95
+ Converts a public key to its compressed format (33 bytes) using 'ecdsa'.
96
+
97
+ Args:
98
+ public_key: The public key in either compressed or uncompressed format.
99
+
100
+ Returns:
101
+ The 33-byte compressed public key.
102
+ """
103
+ try:
104
+ # Create a VerifyingKey from the input bytes.
105
+ vk = VerifyingKey.from_string(public_key, curve=SECP256k1)
106
+ return vk.to_string("compressed")
107
+ except Exception as e:
108
+ raise Secp256k1Error(f"Failed to compress public key: {e}") from e
109
+
110
+
111
+ __all__ = [
112
+ 'private_key_to_public_key',
113
+ 'public_key_to_point_coords',
114
+ 'decompress_public_key',
115
+ 'compress_public_key',
116
+ 'Secp256k1Error',
117
+ ]
libcrypto/wallet.py ADDED
@@ -0,0 +1,135 @@
1
+ """
2
+ High-Level Wallet Interface
3
+
4
+ This module provides a simple, high-level interface for creating cryptocurrency
5
+ wallets from a single private key and generating addresses for various coins.
6
+ """
7
+ from typing import Union, Dict, List
8
+ from .keys import PrivateKey, PublicKey
9
+ from .addresses import AddressGenerator, AddressError
10
+ from .constants import BIP44_COIN_TYPES
11
+
12
+ # Configuration for coins, especially for properties like required public key format.
13
+ COIN_CONFIG = {
14
+ 'ethereum': {'uncompressed_required': True, 'address_types': ['default']},
15
+ 'tron': {'uncompressed_required': True, 'address_types': ['default']},
16
+ 'ripple': {'uncompressed_required': False, 'address_types': ['default']}, # Prefers compressed
17
+ 'bitcoin': {'uncompressed_required': False, 'address_types': ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh']},
18
+ 'litecoin': {'uncompressed_required': False, 'address_types': ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh']},
19
+ 'dogecoin': {'uncompressed_required': False, 'address_types': ['p2pkh']},
20
+ 'dash': {'uncompressed_required': False, 'address_types': ['p2pkh']},
21
+ 'bitcoin_cash': {'uncompressed_required': False, 'address_types': ['p2pkh']},
22
+ 'testnet': {'uncompressed_required': False, 'address_types': ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh']},
23
+ }
24
+
25
+
26
+ class Wallet:
27
+ """
28
+ A simple wallet class to manage a single private key and generate addresses.
29
+
30
+ This class serves as a high-level, easy-to-use wrapper around the library's
31
+ core functionalities. It is initialized with a single private key and can
32
+ generate corresponding addresses for multiple supported cryptocurrencies.
33
+ """
34
+
35
+ def __init__(self, private_key: Union[str, bytes, int, PrivateKey]):
36
+ """
37
+ Initializes the wallet with a private key.
38
+
39
+ Args:
40
+ private_key: The private key in WIF, hex, bytes, integer format,
41
+ or as a PrivateKey object.
42
+ """
43
+ if isinstance(private_key, PrivateKey):
44
+ self.private_key = private_key
45
+ else:
46
+ self.private_key = PrivateKey(private_key)
47
+
48
+ self._public_key_compressed = self.private_key.get_public_key(compressed=True)
49
+ self._public_key_uncompressed = self.private_key.get_public_key(compressed=False)
50
+
51
+ def get_address(self, coin: str, address_type: str = 'p2pkh') -> str:
52
+ """
53
+ Generates a single address for a specific coin and address type.
54
+
55
+ Args:
56
+ coin (str): The name of the coin (e.g., 'bitcoin', 'ethereum').
57
+ address_type (str, optional): The type of address to generate.
58
+ For Bitcoin coins: 'p2pkh' (default), 'p2sh-p2wpkh', 'p2wpkh'.
59
+ For Ethereum coins, this is ignored.
60
+
61
+ Returns:
62
+ str: The generated address string.
63
+
64
+ Raises:
65
+ ValueError: If the coin or address type is not supported.
66
+ NotImplementedError: For coins requiring a different cryptographic curve (e.g., Ed25519).
67
+ """
68
+ coin = coin.lower()
69
+ if coin not in COIN_CONFIG:
70
+ # Check if it's a known coin but just not in the simple config
71
+ if coin in BIP44_COIN_TYPES:
72
+ raise ValueError(f"Coin '{coin}' is recognized but not configured for simple address generation.")
73
+ raise ValueError(f"Unsupported coin: '{coin}'")
74
+
75
+ config = COIN_CONFIG[coin]
76
+
77
+ # Select the correct public key format
78
+ public_key = self._public_key_uncompressed if config['uncompressed_required'] else self._public_key_compressed
79
+
80
+ # For coins with a single address type, override the user's choice
81
+ if len(config['address_types']) == 1 and config['address_types'][0] == 'default':
82
+ addr_type = 'default' # A generic type for coins like ETH, TRX
83
+ else:
84
+ if address_type not in config['address_types']:
85
+ raise ValueError(f"Unsupported address type '{address_type}' for {coin}. "
86
+ f"Available types: {config['address_types']}")
87
+ addr_type = address_type
88
+
89
+ try:
90
+ # In AddressGenerator, 'default' type is handled internally for relevant networks
91
+ # and `addr_type` is used for Bitcoin-style networks.
92
+ effective_type_for_generator = 'default' if addr_type == 'default' else addr_type
93
+ return AddressGenerator.from_public_key(public_key.bytes, effective_type_for_generator, coin)
94
+ except AddressError as e:
95
+ raise ValueError(f"Could not generate address for {coin}: {e}")
96
+
97
+ def get_all_addresses(self, coin: str) -> Dict[str, str]:
98
+ """
99
+ Generates all supported address formats for a given coin.
100
+
101
+ Args:
102
+ coin (str): The name of the coin (e.g., 'bitcoin', 'ethereum').
103
+
104
+ Returns:
105
+ Dict[str, str]: A dictionary where keys are address types and
106
+ values are the corresponding addresses.
107
+ """
108
+ coin = coin.lower()
109
+ if coin not in COIN_CONFIG:
110
+ raise ValueError(f"Unsupported coin: '{coin}'")
111
+
112
+ addresses = {}
113
+ config = COIN_CONFIG[coin]
114
+
115
+ for addr_type in config['address_types']:
116
+ try:
117
+ # Use the main get_address method to ensure consistent logic
118
+ addresses[addr_type] = self.get_address(coin, addr_type)
119
+ except (ValueError, NotImplementedError, AddressError) as e:
120
+ addresses[addr_type] = f"Error: {e}"
121
+
122
+ return addresses
123
+
124
+ @classmethod
125
+ def generate(cls) -> 'Wallet':
126
+ """
127
+ Creates a new Wallet instance with a newly generated private key.
128
+
129
+ Returns:
130
+ A new Wallet instance.
131
+ """
132
+ return cls(PrivateKey.generate())
133
+
134
+ def __repr__(self) -> str:
135
+ return f"Wallet(private_key='{self.private_key.hex}')"