Source code for nostress.core.crypto
"""Cryptographic operations for Nostr key generation."""
import secrets
import base58
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from ..exceptions import CryptographicError
[docs]
def generate_private_key() -> bytes:
"""Generate a secure 32-byte private key.
Returns:
bytes: A secure random 32-byte private key
"""
return secrets.token_bytes(32)
[docs]
def derive_public_key(private_key: bytes) -> bytes:
"""Derive public key from private key using secp256k1.
Args:
private_key: 32-byte private key
Returns:
bytes: 32-byte public key (x-coordinate only)
Raises:
CryptographicError: If key derivation fails
"""
try:
# Convert bytes to integer for private key
private_key_int = int.from_bytes(private_key, byteorder="big")
# Create private key object using cryptography
privkey = ec.derive_private_key(
private_key_int, ec.SECP256K1(), default_backend()
)
# Get public key
pubkey = privkey.public_key()
# Serialize public key in uncompressed format
pubkey_bytes = pubkey.public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint,
)
# Extract x-coordinate (32 bytes) - skip the 0x04 prefix
return pubkey_bytes[1:33]
except Exception as e:
raise CryptographicError(f"Failed to derive public key: {e}") from e
[docs]
def private_key_to_hex(private_key: bytes) -> str:
"""Convert private key to hex format.
Args:
private_key: 32-byte private key
Returns:
str: Hex-encoded private key
"""
return private_key.hex()
[docs]
def public_key_to_hex(public_key: bytes) -> str:
"""Convert public key to hex format.
Args:
public_key: 32-byte public key
Returns:
str: Hex-encoded public key
"""
return public_key.hex()
[docs]
def private_key_to_bech32(private_key: bytes) -> str:
"""Convert private key to bech32 nsec format (NIP-19).
Args:
private_key: 32-byte private key
Returns:
str: Bech32-encoded private key with nsec prefix
Raises:
CryptographicError: If encoding fails
"""
try:
# Simple implementation for now - we'll use a basic approach
# For production, we'd use proper bech32 encoding
return f"nsec{base58.b58encode(private_key).decode()}"
except Exception as e:
raise CryptographicError(f"Failed to encode private key to bech32: {e}") from e
[docs]
def public_key_to_bech32(public_key: bytes) -> str:
"""Convert public key to bech32 npub format (NIP-19).
Args:
public_key: 32-byte public key
Returns:
str: Bech32-encoded public key with npub prefix
Raises:
CryptographicError: If encoding fails
"""
try:
# Simple implementation for now - we'll use a basic approach
# For production, we'd use proper bech32 encoding
return f"npub{base58.b58encode(public_key).decode()}"
except Exception as e:
raise CryptographicError(f"Failed to encode public key to bech32: {e}") from e
[docs]
def generate_keypair() -> tuple[bytes, bytes]:
"""Generate a complete Nostr keypair.
Returns:
Tuple[bytes, bytes]: (private_key, public_key)
"""
private_key = generate_private_key()
public_key = derive_public_key(private_key)
return private_key, public_key
[docs]
def validate_private_key_hex(hex_key: str) -> bool:
"""Validate a hex-encoded private key.
Args:
hex_key: Hex string to validate
Returns:
bool: True if valid, False otherwise
"""
try:
if len(hex_key) != 64: # 32 bytes = 64 hex chars
return False
bytes.fromhex(hex_key)
return True
except ValueError:
return False
[docs]
def validate_public_key_hex(hex_key: str) -> bool:
"""Validate a hex-encoded public key.
Args:
hex_key: Hex string to validate
Returns:
bool: True if valid, False otherwise
"""
try:
if len(hex_key) != 64: # 32 bytes = 64 hex chars
return False
bytes.fromhex(hex_key)
return True
except ValueError:
return False
[docs]
def validate_bech32_key(bech32_key: str, expected_prefix: str) -> bool:
"""Validate a bech32-encoded key.
Args:
bech32_key: Bech32 string to validate
expected_prefix: Expected prefix (nsec or npub)
Returns:
bool: True if valid, False otherwise
"""
try:
if not bech32_key.startswith(expected_prefix):
return False
# Extract the base58 part after the prefix
encoded_part = bech32_key[len(expected_prefix) :]
# Try to decode and check length
decoded = base58.b58decode(encoded_part)
return len(decoded) == 32 # Should be 32 bytes
except Exception:
return False