TallyBox Wallet Generator - Python Edition

by shahiN Noursalehi

You are here: Home / Toturials / Tallybox Wallet Creation - Python Edition

Code Description

This Python script creates or recovers a TallyBox wallet using the secp256r1 elliptic curve. It allows users to either generate a new wallet or recover an existing one by providing a private key. The script generates a wallet address, compresses and encodes the public key, verifies the key pair integrity, and provisions the wallet by encrypting the private key with AES-256-CBC. The wallet details are saved in an XML file named after the wallet (e.g., <wallet_name>.xml). Users must provide a wallet name and a local password to secure the wallet.

tallybox_wallet_creation.py




"""
Tallybox Wallet Provisioning Script
Updated: 2025-04-19

This script provisions a Tallybox wallet (https://tallybox.mixoftix.net) for secure management of 
tokens (e.g., 2PN, 2ZR, TLH) on a DAG network. It generates or recovers a private-public key pair, 
derives a wallet address, and stores the wallet data in an XML file with AES-256-CBC encrypted 
private keys. Features include RFC 6979-compliant ECDSA signatures, key pair verification, and 
Base58-encoded wallet addresses.

Licensed under the GNU General Public License v3 (GPL-3), this software is open-source, ensuring 
freedom to use, modify, and distribute. Derivative works must also be open-source under GPL-3, 
and source code must be provided with distributions.

MixofTix Was Here!
by shahiN Noursalehi
"""

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import utils as crypto_utils
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import hashlib
import base64
import xml.etree.ElementTree as ET
import os
import random
import string
import re
import ecdsa

# Step 1: Generate a private-public key pair
def generate_key_pair():
    """
    Generate a private-public key pair using the secp256r1 curve.
    Returns: (private_key, (x, y)) where (x, y) are the public key coordinates as bytes.
    """
    private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
    public_key = private_key.public_key()

    public_key_bytes = public_key.public_bytes(
        encoding=serialization.Encoding.X962,
        format=serialization.PublicFormat.UncompressedPoint
    )
    x = public_key_bytes[1:33]  # First 32 bytes after 0x04 prefix
    y = public_key_bytes[33:]   # Last 32 bytes

    return private_key, (x, y)

def derive_public_key_from_private(private_key_hex):
    """
    Derive the public key from a given private key (hex string) using secp256r1.
    Args:
        private_key_hex (str): Hex string of the private key.
    Returns: (x, y, private_key) where x, y are coordinates as bytes, private_key is the object.
    """
    private_key_int = int(private_key_hex, 16)
    private_key = ec.derive_private_key(
        private_key_int,
        ec.SECP256R1(),
        default_backend()
    )
    public_key = private_key.public_key()

    public_key_bytes = public_key.public_bytes(
        encoding=serialization.Encoding.X962,
        format=serialization.PublicFormat.UncompressedPoint
    )
    x = public_key_bytes[1:33]
    y = public_key_bytes[33:]

    return x, y, private_key

# Step 2: Compress and Encode the Public Key
def compress_public_key(x, y):
    """
    Compress the public key as per the tutorial.
    Args:
        x (bytes): X-coordinate of the public key (32 bytes).
        y (bytes): Y-coordinate of the public key (32 bytes).
    Returns: (compressed_key, suffix) where compressed_key is a string and suffix is '1' or '2'.
    """
    y_int = int.from_bytes(y, byteorder='big')
    parity = y_int % 2
    suffix = '1' if parity == 1 else '2'
    x_hex = x.hex()
    compressed_key = f"{x_hex}*{suffix}"
    return compressed_key, suffix

def base58_encode(bytes_data):
    """
    Encode a byte array in Base58 as per the tutorial's pseudo-code.
    Args:
        bytes_data (bytes): The byte array to encode.
    Returns: Base58-encoded string.
    """
    alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    value = int.from_bytes(bytes_data, byteorder='big')
    result = ''

    while value > 0:
        remainder = value % 58
        result = alphabet[remainder] + result
        value //= 58

    for byte in bytes_data:
        if byte == 0:
            result = '1' + result
        else:
            break

    return result

def compress_and_encode_public_key(x, y):
    """
    Compress the public key and encode it in Base58.
    Args:
        x (bytes): X-coordinate of the public key.
        y (bytes): Y-coordinate of the public key.
    Returns: (compressed_key, base58_address) where base58_address is the final encoded address.
    """
    compressed_key, suffix = compress_public_key(x, y)
    base58_x = base58_encode(x)
    base58_address = f"{base58_x}*{suffix}"
    return compressed_key, base58_address

# Step 3: Derive the TallyBox Wallet Address
def derive_tallybox_wallet_address(base58_public_key):
    """
    Derive the TallyBox wallet address from the Base58-encoded public key.
    Args:
        base58_public_key (str): The Base58-encoded public key.
    Returns: (final_address, intermediate_steps) where final_address is the wallet address.
    """
    base58_bytes = base58_public_key.encode('utf-8')
    sha256_hash = hashlib.sha256(base58_bytes).hexdigest()
    raw_wallet_string = sha256_hash
    raw_wallet_bytes = bytes.fromhex(raw_wallet_string)
    base58_raw_wallet = base58_encode(raw_wallet_bytes)
    base58_raw_wallet_bytes = base58_raw_wallet.encode('utf-8')
    md5_hash = hashlib.md5(base58_raw_wallet_bytes).hexdigest()
    checksum = 'boxB' + md5_hash[:11]
    final_address = checksum + base58_raw_wallet

    intermediate_steps = {
        'sha256_hash': sha256_hash,
        'raw_wallet_string': raw_wallet_string,
        'raw_wallet_bytes_hex': raw_wallet_bytes.hex(),
        'base58_raw_wallet': base58_raw_wallet,
        'md5_hash': md5_hash,
        'checksum': checksum
    }

    return final_address, intermediate_steps

# Step 4: Verify the Key Pair Integrity with RFC 6979
P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B

def mod_sqrt(a, p):
    """
    Compute the modular square root of a modulo p (Tonelli-Shanks algorithm).
    Args:
        a (int): The number to find the square root of.
        p (int): The prime modulus (p ≡ 3 mod 4 for secp256r1).
    Returns: One of the square roots (the other is -result mod p).
    """
    if pow(a, (p - 1) // 2, p) != 1:
        raise ValueError("No square root exists")

    exponent = (p + 1) // 4
    result = pow(a, exponent, p)
    return result

def decompress_public_key(compressed_key):
    """
    Decompress the public key by computing Y from X and the suffix.
    Args:

        compressed_key (str): The compressed key in the form 'x_hex*suffix'.
    Returns: (x, y) coordinates as integers.
    """
    x_hex, suffix = compressed_key.split('*')
    x = int(x_hex, 16)

    x3 = (x * x * x) % P
    ax = (A * x) % P
    y2 = (x3 + ax + B) % P

    y = mod_sqrt(y2, P)
    y_neg = (-y) % P

    if suffix == '1':
        y_final = y if y % 2 == 1 else y_neg
    else:
        y_final = y if y % 2 == 0 else y_neg

    return x, y_final

def sign_digest(digest, private_key_hex):
    """
    Sign a SHA-256 digest using RFC 6979-compliant ECDSA with secp256r1.
    Args:
        digest (bytes): The SHA-256 digest to sign (32 bytes).
        private_key_hex (str): The private key as a 64-character hex string.
    Returns: Base64-encoded DER signature.
    """
    # Convert private key hex to bytes
    private_key_bytes = bytes.fromhex(private_key_hex)
    
    # Create an ecdsa SigningKey for secp256r1
    sk = ecdsa.SigningKey.from_string(private_key_bytes, curve=ecdsa.NIST256p)
    
    # Sign the digest with RFC 6979 deterministic k
    signature = sk.sign_digest(
        digest,
        sigencode=ecdsa.util.sigencode_der_canonize  # Canonical DER encoding
    )
    
    # Encode signature in Base64
    signature_b64 = base64.b64encode(signature).decode('utf-8')
    return signature_b64

def verify_digest(digest, signature_b64, compressed_key):
    """
    Verify a SHA-256 digest signature using RFC 6979-compliant ECDSA with secp256r1.
    Args:
        digest (bytes): The SHA-256 digest that was signed (32 bytes).
        signature_b64 (str): Base64-encoded DER signature.
        compressed_key (str): The compressed public key (x_hex*suffix).
    Returns: True if the signature is valid, False otherwise.
    """
    try:
        # Decompress the public key to get x, y coordinates
        x_int, y_int = decompress_public_key(compressed_key)
        x_bytes = x_int.to_bytes(32, byteorder='big')
        y_bytes = y_int.to_bytes(32, byteorder='big')
        
        # Create an ecdsa VerifyingKey
        public_key_bytes = b'\x04' + x_bytes + y_bytes
        vk = ecdsa.VerifyingKey.from_string(
            public_key_bytes,
            curve=ecdsa.NIST256p
        )
        
        # Decode the Base64 signature
        signature = base64.b64decode(signature_b64)
        
        # Verify the signature
        return vk.verify_digest(
            signature,
            digest,
            sigdecode=ecdsa.util.sigdecode_der
        )
    except Exception:
        return False

def verify_key_pair_integrity(private_key, compressed_key):
    """
    Verify the key pair integrity by signing and verifying a test message using RFC 6979.
    Args:
        private_key: The private key object (from cryptography).
        compressed_key (str): The compressed public key from Step 2 (x_hex*suffix).
    Returns: (is_valid, signature_base64, decompressed_key) for verification.
    """
    # Test message to sign
    message = "TallyBox, a tool for curious minds..".encode('utf-8')
    digest = hashlib.sha256(message).digest()
    
    # Get private key as hex
    private_key_bytes = private_key.private_numbers().private_value.to_bytes(32, byteorder='big')
    private_key_hex = private_key_bytes.hex()
    
    # Sign the digest using RFC 6979
    signature_b64 = sign_digest(digest, private_key_hex)
    
    # Verify the signature
    is_valid = verify_digest(digest, signature_b64, compressed_key)
    
    # Decompress the public key for reporting
    x_int, y_int = decompress_public_key(compressed_key)
    x_bytes = x_int.to_bytes(32, byteorder='big')
    y_bytes = y_int.to_bytes(32, byteorder='big')
    decompressed_key = f"{x_bytes.hex()}*{y_bytes.hex()}"
    
    return is_valid, signature_b64, decompressed_key

# Step 5: Upgraded AES Functions
def aes256_cbc_encrypt_js_compatible(data: str, secret: str, show_logs: bool = False) -> str:
    """
    Encrypt data using AES-256-CBC to match Java's AES_Encrypt_by_secret_with_custom_padding.
    Args:
        data (str): The data to encrypt (string, UTF-8 encoded).
        secret (str): Secret key (at least 64 characters, hex string).
        show_logs (bool): Whether to print debug logs.
    Returns: Base64-encoded ciphertext (no IV prepended, may include newlines).
    """
    if len(secret) < 64:
        raise ValueError("Secret key must be at least 64 characters")
    if not re.match(r'^[0-9a-fA-F]{64,}$', secret):
        raise ValueError("Secret key must be a hex string")

    aes_password = secret[:32].encode('ascii')  # Match Java's password.toCharArray() (UTF-8 equivalent)
    aes_iv = secret[32:48].encode('ascii')     # Match Java's aes_iv.getBytes("ASCII")
    aes_salt = secret[48:64].encode('ascii')   # Match Java's salt.getBytes() (UTF-8 equivalent)

    key = hashlib.pbkdf2_hmac('sha256', aes_password, aes_salt, 3, 32)

    if show_logs:
        print(f"Encrypt - Secret: {secret}")
        print(f"Encrypt - Key (hex): {key.hex()}")
        print(f"Encrypt - IV (hex): {aes_iv.hex()}")

    left_padding_size = random.randint(0, 99)
    left_padding = ''.join(random.choice(string.ascii_letters) for _ in range(left_padding_size))
    right_padding_size = random.randint(0, 99)
    right_padding = ''.join(random.choice(string.ascii_letters) for _ in range(right_padding_size))
    padded_data = f"{left_padding}|{data}|{right_padding}".encode('utf-8')

    padding_length = 16 - (len(padded_data) % 16)
    padded_data += bytes([padding_length] * padding_length)

    cipher = Cipher(algorithms.AES(key), modes.CBC(aes_iv), backend=default_backend())
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(padded_data) + encryptor.finalize()

    # Match Java's Base64.DEFAULT (may include newlines for long strings)
    ciphertext_b64 = base64.b64encode(ciphertext).decode('utf-8')
    return ciphertext_b64

def aes256_cbc_decrypt_js_compatible(encrypted_base64: str, secret: str, show_logs: bool = False) -> str:
    """
    Decrypt Base64-encoded AES-256-CBC data to match Java's AES_Decrypt_by_secret_with_custom_padding.
    Args:
        encrypted_base64 (str): Base64-encoded ciphertext (no IV prepended, may include newlines).
        secret (str): Secret key (at least 64 characters, hex string).
        show_logs (bool): Whether to print debug logs.
    Returns: Decrypted data as string.
    """
    if len(secret) < 64:
        raise ValueError("Secret key must be at least 64 characters")
    if not re.match(r'^[0-9a-fA-F]{64,}$', secret):
        raise ValueError("Secret key must be a hex string")

    try:
        # Handle Java's Base64.DEFAULT (strip newlines if present)
        ciphertext = base64.b64decode(encrypted_base64.replace('\n', ''))
        if show_logs:
            print(f"Decrypt - Ciphertext length: {len(ciphertext)}")
    except Exception as e:
        raise ValueError(f"Invalid Base64 ciphertext: {e}")

    aes_password = secret[:32].encode('ascii')  # Match Java's password.toCharArray() (UTF-8 equivalent)
    aes_iv = secret[32:48].encode('ascii')     # Match Java's aes_iv.getBytes("ASCII")
    aes_salt = secret[48:64].encode('ascii')   # Match Java's salt.getBytes() (UTF-8 equivalent)

    key = hashlib.pbkdf2_hmac('sha256', aes_password, aes_salt, 3, 32)

    if show_logs:
        print(f"Decrypt - Secret: {secret}")
        print(f"Decrypt - Key (hex): {key.hex()}")
        print(f"Decrypt - IV (hex): {aes_iv.hex()}")

    cipher = Cipher(algorithms.AES(key), modes.CBC(aes_iv), backend=default_backend())
    decryptor = cipher.decryptor()
    try:
        padded_data = decryptor.update(ciphertext) + decryptor.finalize()
    except Exception as e:
        raise ValueError(f"Decryption failed: {e}")

    try:
        padding_length = padded_data[-1]
        if padding_length > 16 or padding_length == 0:
            raise ValueError("Invalid PKCS#7 padding")
        padded_data = padded_data[:-padding_length]
    except IndexError:
        raise ValueError("Invalid padding length")

    if show_logs:
        print(f"Decrypt - Padded data (hex): {padded_data.hex()}")

    try:
        padded_text = padded_data.decode('utf-8')
        if show_logs:
            print(f"Decrypt - Padded text: {padded_text}")
        parts = padded_text.split('|')
        if len(parts) != 3:
            raise ValueError("Invalid padding format in decrypted text")
        plaintext = parts[1]
    except UnicodeDecodeError:
        if show_logs:
            print("Decrypt - Warning: Decrypted data is not UTF-8, attempting byte split")
        parts = padded_data.split(b'|')
        if len(parts) != 3:
            raise ValueError("Invalid padding format in decrypted bytes")
        try:
            plaintext = parts[1].decode('ascii')
            if show_logs:
                print(f"Decrypt - Extracted plaintext: {plaintext}")
        except UnicodeDecodeError:
            raise ValueError("Decrypted plaintext is not a valid ASCII string")

    if not re.match(r'^[0-9a-fA-F]{64}$', plaintext):
        raise ValueError(f"Decrypted private key is not a 64-character hex string: {plaintext}")

    return plaintext

# Step 6: Provision the TallyBox Wallet
def indent(elem, level=0):
    """
    Helper function to pretty-print XML with indentation.
    Args:
        elem: The XML element to indent.
        level: The current indentation level.
    """
    i = "\n" + level * "  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for child in elem:
            indent(child, level + 1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i

def provision_tallybox_wallet(private_key, wallet_address, base58_public_key, wallet_name, password, show_logs: bool = False):
    """
    Provision the TallyBox wallet by securely storing its data.
    Args:
        private_key: The private key object (from cryptography).
        wallet_address (str): The TallyBox wallet address from Step 3.
        base58_public_key (str): The Base58-encoded public key from Step 2.
        wallet_name (str): The wallet name provided by the user.
        password (str): The local password provided by the user.
        show_logs (bool): Whether to print AES debug logs.
    Returns: (encrypted_private_key_b64, xml_str, verification_result, private_key_bytes, secret) for verification.
    """
    # 1. Form the key string using the provided wallet name and password
    key_string = f"{wallet_name}~{password}~{wallet_address}"

    # 2. Compute the SHA-256 hash of the key string to generate the secret
    secret = hashlib.sha256(key_string.encode()).hexdigest()

    # 3. Encrypt the private key as a hex string
    private_key_bytes = private_key.private_numbers().private_value.to_bytes(32, byteorder='big')
    private_key_hex = private_key_bytes.hex()
    encrypted_private_key_b64 = aes256_cbc_encrypt_js_compatible(private_key_hex, secret, show_logs=show_logs)

    # 4. Decrypt to verify correctness
    decrypted_private_key_hex = aes256_cbc_decrypt_js_compatible(encrypted_private_key_b64, secret, show_logs=show_logs)
    verification_result = (decrypted_private_key_hex == private_key_hex)

    # 5. Create the XML file content
    root = ET.Element("tallybox_wallet")

    ET.SubElement(root, "wallet_name").text = wallet_name
    ET.SubElement(root, "wallet_address").text = wallet_address
    ET.SubElement(root, "public_key_b58_compressed").text = base58_public_key
    ET.SubElement(root, "private_key_aes_b64").text = encrypted_private_key_b64
    ET.SubElement(root, "tech_info").text = "https://mailarchive.ietf.org/arch/msg/ideas/VEzD4RXKlCFIUftYIrrEdMFgoW0/"

    # Pretty print the XML
    indent(root)
    xml_str = "\n"
    xml_str += ET.tostring(root, encoding='unicode', method='xml')

    # 6. Write the XML to a file named .xml
    xml_filename = f"{wallet_name}.xml"
    with open(xml_filename, "w", encoding="utf-8") as f:
        f.write(xml_str)

    return encrypted_private_key_b64, xml_str, verification_result, private_key_bytes, secret

def main():
    # Step 1: Prompt user to select an option
    print("Select an option:")
    print("A) Generate Wallet")
    print("B) Recover Wallet")
    option = input("Enter your choice (A or B): ").strip().upper()

    # Validate the option
    while option not in ['A', 'B']:
        print("Invalid choice. Please select A or B.")
        option = input("Enter your choice (A or B): ").strip().upper()

    # Step 2: Get wallet name, password, and debug log preference
    wallet_name = input("Enter wallet name: ").strip()
    password = input("Enter local password: ").strip()
    debug_choice = input("Show AES debug logs? (y/N): ").strip().lower()
    show_logs = debug_choice in ['y', 'yes']

    # Step 3: Generate or recover the private-public key pair based on the option
    if option == 'A':
        # Generate a new private-public key pair
        print("Generating a new wallet...")
        private_key, (x, y) = generate_key_pair()
    else:
        # Recover wallet using an existing private key
        print("Recovering wallet from private key...")
        private_key_hex = input("Enter your existing private key (in hex format, 64 characters): ").strip()
        # Basic validation for the private key (should be 64 hex characters)
        while len(private_key_hex) != 64 or not all(c in '0123456789abcdefABCDEF' for c in private_key_hex):
            print("Invalid private key. It must be a 64-character hexadecimal string.")
            private_key_hex = input("Enter your existing private key (in hex format, 64 characters): ").strip()
        x, y, private_key = derive_public_key_from_private(private_key_hex)

    # Step 4: Compress and encode the public key
    compressed_key, base58_public_key = compress_and_encode_public_key(x, y)

    # Step 5: Derive the TallyBox wallet address
    final_address, steps = derive_tallybox_wallet_address(base58_public_key)

    # Print Step 5 results
    print("\nStep 5: TallyBox Wallet Address Derivation")
    if show_logs:
        print(f"Base58 Public Key: {base58_public_key}")
        print(f"SHA-256 Hash: {steps['sha256_hash']}")
        print(f"Raw Wallet Bytes (hex): {steps['raw_wallet_bytes_hex']}")
        print(f"Base58 Raw Wallet: {steps['base58_raw_wallet']}")
        print(f"MD5 Hash: {steps['md5_hash']}")
        print(f"Checksum: {steps['checksum']}")
    print(f"Final Wallet Address: {final_address}")

    # Step 6: Verify the key pair integrity with RFC 6979
    is_valid, signature_base64, decompressed_key = verify_key_pair_integrity(private_key, compressed_key)

    # Step 7: Provision the TallyBox wallet
    encrypted_private_key_b64, xml_str, verification_result, private_key_bytes, secret = provision_tallybox_wallet(
        private_key, final_address, base58_public_key, wallet_name, password, show_logs=show_logs
    )

    # Step 8: Print the results for Step 7
    print("\nStep 7 Results:")
    if show_logs:
        print("Key String (pre-SHA-256):", f"{wallet_name}~{password}~{final_address}")
        print("Local Key (SHA-256):", secret)
        print("Original Private Key (hex):", private_key_bytes.hex())
        print("Encrypted Private Key (Base64):", encrypted_private_key_b64)
        decrypted_private_key = aes256_cbc_decrypt_js_compatible(encrypted_private_key_b64, secret, show_logs=show_logs)
        print("Decrypted Private Key (hex):", decrypted_private_key)
    print("Verification Result:", "Verified" if verification_result else "Not Verified")
    print("\nXML Content:")
    print(xml_str)
    print(f"\nXML data has been saved to {wallet_name}.xml")

if __name__ == "__main__":
    main()
	
	

Example Output:


Required Dependencies and Setup

To run this script, ensure you have Python 3.7 or higher installed. The following dependencies are required:

  • ecdsa: Install using
    pip install ecdsa
    . This library provides RFC 6979-compliant ECDSA signatures for key pair verification.
  • cryptography: Install using
    pip install cryptography
    . This library provides cryptographic primitives (e.g., secp256r1, AES-256-CBC) for key generation and encryption.
  • Standard Libraries: The script uses hashlib, base64, xml.etree.ElementTree, os, random, string, and re, which are included in Python's standard library.

Setup Instructions:

  1. Install Python 3.7+ from python.org.
  2. Install the required packages by running:
    pip install ecdsa cryptography
    .
  3. Ensure your working directory has write permissions, as the script will save an XML file (<wallet_name>.xml).
Alternative: You can run the script using online interpreters like Google Colab, which supports Python 3.7+ and the required packages.


Important Warning: Save Your Private Key and Password

Critical: When generating a new wallet with debug logs enabled (by selecting 'y' for "Show AES debug logs?"), the script will display your private key in the output (labeled "Original Private Key (hex)"). You must write down this private key and the local password you provided. These are required to recover your wallet later if needed. Store them securely offline (e.g., on paper or an encrypted drive). Losing them may result in permanent loss of access to your wallet.

Acknowledgments

Special thanks to Grok, for its invaluable assistance in creating this TallyBox wallet creation tutorial.