import hashlib
import binascii
from hashlib import scrypt
from Cryptodome.Cipher import AES
from .utils.keys import PrivateKey
from .utils import base58
# We don't use our custom AES encryption functions because BIP38
# has specific requirements for the encryption.
[docs]def encrypt(raw, passphrase):
"""
Encrypts data with a passphrase.
This method should only be used for calculating BIP38 data.
It uses AES settings that are not ideal for general-purpose use.
Parameters:
raw (str): The text to encrypt.
passphrase (str): The passphrase used for encryption.
Returns:
bytes: The encrypted text.
"""
cipher = AES.new(passphrase, AES.MODE_CBC, b"\x00" * 16)
ciphertext = cipher.encrypt(raw)
return ciphertext
[docs]def decrypt(enc, passphrase):
"""
Decrypts encrypted data with a passphrase.
This method should only be used for calculating BIP38 data.
It uses AES settings that are not ideal for general-purpose use.
Parameters:
enc (bytes): The encrypted text to decrypt.
passphrase (str): The passphrase used for decryption.
Returns:
bytes: The decrypted text.
"""
ciphertext = enc
cipher = AES.new(passphrase, AES.MODE_CBC, b"\x00" * 16)
plaintext = cipher.decrypt(ciphertext)
return plaintext.rstrip(b"\0")
[docs]class Bip38PrivateKey:
"""
Represents a BIP38 encrypted private key for Bitcoin.
"""
BLOCK_SIZE = 16
KEY_LEN = 32
IV_LEN = 16
[docs] def __init__(
self,
privkey: PrivateKey,
passphrase,
compressed=True,
segwit=False,
witness_version=0,
):
"""Creates a BIP0038 private key with non-ec-multiply encryption.
Args:
privkey (PrivateKey): The private key.
passphrase (_type_): The encryption passphrase
compressed (bool, optional): Whether to use compressed public keys. Defaults to True.
segwit (bool, optional): Whether to use segwit address. Defaults to False.
witness_version (int, optional): The witness version for generating the segwit address. Defaults to 0.
Raises:
ValueError: If the private key object does not support Base58 or Bech32.
"""
# BIP0038 non-ec-multiply encryption. Returns BIP0038 encrypted privkey.
if (
"BASE58" not in privkey.network.ADDRESS_MODE
and "BECH32" not in privkey.network.ADDRESS_MODE
):
raise ValueError("BIP38 requires Base58 or Bech32 addresses")
flagbyte = b"\xe0" if compressed else b"\xc0"
addr = (
privkey.public_key.bech32_address(compressed, witness_version)
if segwit
else privkey.public_key.base58_address(compressed)
)
addresshash = hashlib.sha256(hashlib.sha256(addr.encode()).digest()).digest()[
0:4
]
key = scrypt(
passphrase.encode("utf-8"), salt=addresshash, n=16384, r=8, p=8, dklen=64
)
derivedhalf1 = key[0:32]
derivedhalf2 = key[32:64]
encryptedhalf1 = encrypt(
binascii.unhexlify(
"%0.32x"
% (
int(binascii.hexlify(bytes(privkey)[0:16]), 16)
^ int(binascii.hexlify(derivedhalf1[0:16]), 16)
)
),
derivedhalf2,
)
encryptedhalf2 = encrypt(
binascii.unhexlify(
"%0.32x"
% (
int(binascii.hexlify(bytes(privkey)[16:32]), 16)
^ int(binascii.hexlify(derivedhalf1[16:32]), 16)
)
),
derivedhalf2,
)
self.flagbyte = flagbyte
self.addresshash = addresshash
self.encryptedhalf1 = encryptedhalf1
self.encryptedhalf2 = encryptedhalf2
encrypted_privkey = (
b"\x01\x42"
+ self.flagbyte
+ self.addresshash
+ self.encryptedhalf1
+ self.encryptedhalf2
)
encrypted_privkey += hashlib.sha256(
hashlib.sha256(encrypted_privkey).digest()
).digest()[
:4
] # b58check for encrypted privkey
self._encrypted_privkey = base58.b58encode(encrypted_privkey)
@property
def base58(self):
"""Returns the Base58 representation of the encrypted private key."""
return self._encrypted_privkey.decode()
[docs] def private_key(self, passphrase, compressed=True, segwit=False, witness_version=0):
"""Decrypts the encrypted private key using the given passphrase and returns the corresponding WIF private key.
Args:
passphrase (_type_): The passphrase to decrypt the key.
compressed (bool, optional): Whether to use compressed public keys. Defaults to True.
segwit (bool, optional): Whether to use segwit address. Defaults to False.
witness_version (int, optional): The witness version for generating the segwit address. Defaults to 0.
Raises:
ValueError: If the wrong decryption passphrase was supplied.
Returns:
PrivateKey: a Bitcoin private key.
"""
# BIP0038 non-ec-multiply decryption. Returns WIF privkey.
d = base58.b58decode(self._encrypted_privkey)
# The flagbyte is the 3rd byte in this array.
# 0xc0 means uncompressed and 0xe0 means compressed.
# But we don't use that because the user can choose
# the compression themselves.
d = d[3:]
addresshash = d[0:4]
d = d[4:-4]
key = scrypt(
passphrase.encode("utf-8"), salt=addresshash, n=16384, r=8, p=8, dklen=64
)
derivedhalf1 = key[0:32]
derivedhalf2 = key[32:64]
encryptedhalf1 = d[0:16]
encryptedhalf2 = d[16:32]
decryptedhalf2 = decrypt(encryptedhalf2, derivedhalf2)
decryptedhalf1 = decrypt(encryptedhalf1, derivedhalf2)
priv = decryptedhalf1 + decryptedhalf2
priv = PrivateKey.from_bytes(
binascii.unhexlify(
"%064x"
% (
int(binascii.hexlify(priv), 16)
^ int(binascii.hexlify(derivedhalf1), 16)
)
)
)
pub = priv.public_key
addr = (
pub.bech32_address(compressed, witness_version)
if segwit
else pub.base58_address(compressed)
)
if (
hashlib.sha256(hashlib.sha256(addr.encode()).digest()).digest()[0:4]
!= addresshash
):
raise ValueError("Verification failed. Password is incorrect.")
else:
return priv