frostfs-sdk-python/frostfs_sdk/cryptography/key_extension.py
Ilyas Niyazov 297e107b10 [#3] Added generate proto script create container method
Signed-off-by: Ilyas Niyazov <i.niyazov@yadro.com>
2025-03-25 11:13:48 +03:00

100 lines
3.7 KiB
Python

import base58
import ecdsa
import hashlib
from struct import pack, unpack
from Crypto.Hash import RIPEMD160
COMPRESSED_PUBLIC_KEY_LENGTH = 33
NEO_ADDRESS_VERSION = 0x35
UNCOMPRESSED_PUBLIC_KEY_LENGTH = 65
DECODE_ADDRESS_LENGTH = 21
PS_IN_HASH160 = 0x0C
CHECK_SIG_DESCRIPTOR = int.from_bytes(
hashlib.sha256("System.Crypto.CheckSig".encode('ascii')).digest()[:4], byteorder='little'
)
class KeyExtension:
def get_private_key_from_wif(self, wif: str) -> bytes:
"""
Converts a WIF private key to a byte array.
:param wif: WIF private key in string format.
:return: Private key in byte format (32 bytes).
:raises ValueError: If the WIF key is incorrect.
"""
assert not self.is_empty(wif)
decoded = base58.b58decode_check(wif)
if len(decoded) != 34 or decoded[0] != 0x80 or decoded[-1] != 0x01:
raise ValueError("Incorrect WIF private key")
private_key = decoded[1:-1]
return private_key
def get_public_key(self, private_key: bytes) -> bytes:
"""
Extract public key from Private key
:param private_key: Private key in byte format (32 bytes).
:return: compressed public key in byte format (33 bytes).
:raises ValueError: If the private_key key is empty or null.
"""
assert not self.is_empty(private_key)
if len(private_key) != 32:
raise ValueError(f"Incorrect len of private key, Expected: 32, Actual: {len(private_key)}")
public_key = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.NIST256p).get_verifying_key()
compressed_public_key = public_key.to_string("compressed")
return compressed_public_key
def get_owner_id_by_public_key(self, public_key: bytes) -> str:
if len(public_key) != COMPRESSED_PUBLIC_KEY_LENGTH:
raise ValueError(f"Encoded compressed public key has wrong length. Expected {COMPRESSED_PUBLIC_KEY_LENGTH}, got {len(public_key)}")
script_hash = self.get_script_hash(public_key)
data = bytearray(DECODE_ADDRESS_LENGTH)
data[0] = NEO_ADDRESS_VERSION
data[1:] = script_hash
return base58.b58encode_check(data).decode('utf-8')
def get_script_hash(self, public_key: bytes):
script = self.create_signature_redeem_script(public_key)
sha256_hash = hashlib.sha256(script).digest()
return self.get_ripemd160(sha256_hash)
@staticmethod
def create_signature_redeem_script(public_key: bytes):
if len(public_key) != COMPRESSED_PUBLIC_KEY_LENGTH:
raise ValueError(f"Encoded compressed public key has wrong length. Expected {COMPRESSED_PUBLIC_KEY_LENGTH}, got {len(public_key)}")
script = bytearray([PS_IN_HASH160, COMPRESSED_PUBLIC_KEY_LENGTH])
script.extend(public_key)
script.append(UNCOMPRESSED_PUBLIC_KEY_LENGTH)
script.extend(pack("<I", CHECK_SIG_DESCRIPTOR))
return bytes(script)
@staticmethod
def get_ripemd160(value):
if value is None:
raise ValueError("Input parameter is missing")
digest = RIPEMD160.new()
digest.update(value)
return digest.digest()
@staticmethod
def is_empty(sequence_symbols: bytes | str):
if len(sequence_symbols) == 0 or sequence_symbols is None:
raise ValueError(f"Empty sequence symbols of key: {sequence_symbols}")
return False
@staticmethod
def get_hex_string(value):
if value is None or len(value) == 0:
raise ValueError("Input parameter is missing")
# Convert byte array to hexadecimal string
return f"{int.from_bytes(value, byteorder='big'):0{len(value) * 2}x}"