diff --git a/README.md b/README.md
index 9ccde2f..4de62bd 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,8 @@ deactivate
The wallet client provides a CLI for managing and decrypting wallet data.
The CLI supports various sub-commands (`generatewallet`, `generateaddress`, and `decryptwallet`) and their corresponding options.
+*Note: To ensure a high level of security, this wallet client is designed with an auto-delete feature for encrypted wallets. After 10 unsuccessful password attempts, the wallet will be automatically deleted in order to protect its contents and safeguard against unauthorized access. (For more details, please refer to: [feat: Wallet Annihilation](https://github.com/The-Sycorax/DenaroWalletClient/commit/e347b6622d47415ddc531e8b3292c96b42128c9a))*
+
### Sub-Commands:
-
@@ -258,6 +260,12 @@ python3 wallet_client.py decryptwallet -wallet=wallet.json -password=MySecurePas
------------
+## Disclaimer:
+
+Neither The-Sycorax nor contributors of this project assume liability for any loss of funds incurred through the use of this software! This software is provided 'as is' under the [MIT License](LICENSE) without guarantees or warrenties of any kind, express or implied. It is strongly recommended that users back up their cryptographic keys. User are solely responsible for the security and management of their assets! The use of this software implies acceptance of all associated risks, including financial losses, with no liability on The-Sycorax or contributors of this project.
+
+------------
+
## License:
The Denaro Wallet Client is released under the terms of the MIT license. See [LICENSE](LICENSE) for more
information or see https://opensource.org/licenses/MIT.
diff --git a/denaro/wallet/cryptographic_util.py b/denaro/wallet/cryptographic_util.py
deleted file mode 100644
index 9857723..0000000
--- a/denaro/wallet/cryptographic_util.py
+++ /dev/null
@@ -1,935 +0,0 @@
-import os
-import logging
-import base64
-import hashlib
-import random
-from Crypto.Cipher import AES, ChaCha20_Poly1305
-from Crypto.Protocol.KDF import scrypt
-import hmac as hmac_module
-import pyotp
-import ctypes
-import json
-import time
-import sys
-from filelock import Timeout, FileLock
-
-# Global variables
-FAILED_ATTEMPTS = 0
-MAX_ATTEMPTS = 5
-DIFFICULTY = 3
-
-class EncryptionUtils:
- """
- Handles encryption and decryption tasks.
- """
- @staticmethod
- def aes_gcm_encrypt(data, key, nonce):
- cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
- ciphertext, tag = cipher.encrypt_and_digest(data)
- result = ciphertext, tag
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def aes_gcm_decrypt(ciphertext, tag, key, nonce):
- cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
- result = cipher.decrypt_and_verify(ciphertext, tag)
- return result
-
- @staticmethod
- def chacha20_poly1305_encrypt(data, key):
- cipher = ChaCha20_Poly1305.new(key=key)
- ciphertext, tag = cipher.encrypt_and_digest(data)
- result = cipher.nonce, ciphertext, tag
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def chacha20_poly1305_decrypt(nonce, ciphertext, tag, decryption_key):
- """
- Decrypt data using ChaCha20-Poly1305.
- """
- cipher = ChaCha20_Poly1305.new(key=decryption_key, nonce=nonce)
- decrypted_data = cipher.decrypt(ciphertext)
- try:
- cipher.verify(tag)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not decrypted_data])
- return decrypted_data
- except ValueError:
- logging.error("ChaCha20-Poly1305 tag verification failed. Data might be corrupted or tampered with.")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- raise ValueError("ChaCha20-Poly1305 tag verification failed. Data might be corrupted or tampered with.")
-
-class ProofOfWork:
- """
- Handles proof-of-work generation and validation.
- """
- @staticmethod
- def generate_proof(challenge):
- proof = 0
- target = "1" * DIFFICULTY
- while not hashlib.sha256(challenge + str(proof).encode()).hexdigest().startswith(target):
- proof += 1
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not proof])
- return proof
-
- @staticmethod
- def is_proof_valid(proof, challenge):
- result = hashlib.sha256(challenge + str(proof).encode()).hexdigest().startswith("1" * DIFFICULTY)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
-class DataManipulation:
- """
- Handles data scrambling and descrambling.
- """
- dot_count = 0
- iteration_count = 0
-
- @staticmethod
- def scramble(data, seed):
- if isinstance(seed, int):
- seed = seed.to_bytes((seed.bit_length() + 7) // 8, 'big')
- random.seed(hashlib.sha256(seed).digest())
- indices = list(range(len(data)))
- random.shuffle(indices)
- scrambled_data = bytearray(len(data))
- for i, j in enumerate(indices):
- scrambled_data[j] = data[i]
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not scrambled_data])
- return scrambled_data
-
- @staticmethod
- def descramble(scrambled_data, seed):
- if isinstance(seed, int):
- seed = seed.to_bytes((seed.bit_length() + 7) // 8, 'big')
- random.seed(hashlib.sha256(seed).digest())
- indices = list(range(len(scrambled_data)))
- random.shuffle(indices)
- data = bytearray(len(scrambled_data))
- for i, j in enumerate(indices):
- data[i] = scrambled_data[j]
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not data])
- return data
-
- @staticmethod
- def update_or_reset_attempts(data, filename, hmac_salt, password_verified, deterministic):
- """
- Updates or resets failed login attempts based on whether the password was verified.
-
- Arguments:
- - data: The wallet data
- - hmac_salt: The HMAC salt
- - password_verified: Boolean indicating if the password is verified
- - filename: The name of the wallet file
- - deterministic: Boolean indicating if the wallet is deterministic
- """
- # Determine the appropriate function to update or reset attempts
- update_or_reset = CryptoWallet.update_failed_attempts if not password_verified else CryptoWallet.reset_failed_attempts
-
- # Define keys to update or reset based on deterministic flag
- key_list = [["entry_data", "entries"]]
- if deterministic:
- key_list.append(["entry_data", "key_data"])
- key_list.append(["totp_secret"])
-
- # Update or reset the attempts for each key in the wallet data
- for key in key_list:
- # Initialize target_data as the root dictionary
- target_data = data["wallet_data"]
- for k in key:
- # Navigate through nested keys
- target_data = target_data.get(k, {})
-
- # Convert to list if target_data is not a list
- if not isinstance(target_data, list):
- target_data = [target_data]
-
- # Convert each entry to string
- target_data = [str(entry) for entry in target_data]
-
- # Update or reset attempts
- updated_data, attempts_left = update_or_reset(target_data, hmac_salt)
-
- # Save the updated data back into the original data structure
- if len(key) == 1:
- data["wallet_data"][key[0]] = updated_data
- else:
- data["wallet_data"][key[0]][key[1]] = updated_data
-
- data["wallet_data"]["totp_secret"] = data["wallet_data"]["totp_secret"][0]
- if not password_verified:
- if attempts_left:
- print(f"\nPassword Attempts Left: {attempts_left}")
- if attempts_left <= 5 and attempts_left > 3 and attempts_left != 0:
- logging.warning(f"Password attempts left are approaching 0, after which any existing wallet data will be ERASED AND POTENTIALLY UNRECOVERABLE!!")
- if attempts_left <= 3 and attempts_left != 0:
- logging.critical(f"PASSWORD ATTEMPTS LEFT ARE APPROACHING 0, AFTER WHICH ANY EXISTING WALLET DATA WILL BE ERASED AND POTENTIALLY UNRECOVERABLE!!")
- if attempts_left == 0:
- print(f"\nPassword Attempts Left: {attempts_left}")
- DataManipulation.delete_wallet(filename, data)
- print("Wallet data has been permanetly erased.")
- time.sleep(0.5)
- data = None
-
- # Securely delete sensitive variables
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not data])
- return data
-
- @staticmethod
- def secure_delete(var):
- """Overview:
- This function aims to securely delete a variable by overwriting its memory footprint with zeros, thus ensuring
- that sensitive data does not linger in memory. By employing both native Python techniques and lower-level memory
- operations, it ensures that sensitive data remnants are minimized, reducing exposure to potential threats.
-
- Arguments:
- - var (various types): The variable which needs to be securely deleted and its memory overwritten.
-
- Returns:
- - None: This function works by side-effect, modifying memory directly.
- """
- try:
- # Attempt to get the memory size of the variable using ctypes
- var_size = ctypes.sizeof(var)
- # Create a byte array of zeros with the size of the variable
- zeros = (ctypes.c_byte * var_size)()
- # Retrieve the memory address of the variable
- var_address = id(var)
- # Overwrite the variable's memory location with zeros
- ctypes.memmove(var_address, zeros, var_size)
- except TypeError:
- # Handle different types of variables
- if isinstance(var, (str, bytes)):
- # For immutable types, reassigning is the only way, but it's not 100% secure
- var = '0' * len(var)
- elif isinstance(var, list):
- # For lists, we can zero out each element
- for i in range(len(var)):
- var[i] = 0
- elif isinstance(var, dict):
- # For dictionaries, set each value to zero
- for key in var:
- var[key] = 0
- else:
- # For other unsupported types, just reassign to zero
- var = 0
- finally:
- # Explicitly delete the variable reference
- del var
-
- @staticmethod
- def _save_data(filename, data):
- """
- Persistently stores wallet data to a specified file.
- """
- try:
- with open(filename, 'w') as f:
- if data:
- json.dump(data, f, indent=4)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- else:
- f = data
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- except Exception as e:
- logging.error(f"Error saving data to file: {str(e)}")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
-
- @staticmethod
- def overwrite_with_pattern(file, pattern, file_size):
- """Overview:
- This function is designed for secure file overwriting. It methodically replaces the content
- of a file with a given pattern. It continuously writes the pattern to the file, ensuring complete
- coverage of the original data. The function also includes progress logging and strict data integrity
- measures, like buffer flushing and disk state synchronization.
-
- Parameters:
- - file: A binary-write-mode file object.
- - pattern: Byte string used as the overwrite pattern.
- - file_size: Total size of the file to be overwritten, in bytes.
- """
- try:
- file.seek(0) # Start at the beginning of the file
- bytes_written = 0
-
- # Update interval for printing progress, can be adjusted for efficiency
- update_interval = max(1, file_size // 1)
-
- while bytes_written < file_size:
- write_size = min(file_size - bytes_written, len(pattern))
- file.write(pattern[:write_size])
- bytes_written += write_size
-
- # Update progress at intervals
- if bytes_written % update_interval == 0 or bytes_written == file_size:
- DataManipulation.dot_count += 1
- if DataManipulation.dot_count >= 4:
- DataManipulation.dot_count = 0
- DataManipulation.iteration_count += 1
- if DataManipulation.iteration_count > 4:
- DataManipulation.iteration_count = 1
- sys.stdout.write("\r" + " " * 50) # Clear with 50 spaces
- sys.stdout.write("\rWallet Annihilation in progress" + "." * DataManipulation.iteration_count)
- sys.stdout.flush()
-
- file.flush()
- os.fsync(file.fileno())
- except IOError as e:
- print()
- logging.error(f"IOError during file overwrite: {e}")
- except Exception as e:
- print()
- logging.error(f"Unexpected error during file overwrite: {e}")
-
- @staticmethod
- def DoD_5220_22_M_wipe(file, file_size):
- """Overview:
- Implements the DoD 5220.22-M wiping standard, a recognized method for secure file erasure.
- This function executes multiple overwrite passes, using a mix of zero bytes, one bytes, and
- random data. Each pass serves to further obfuscate the underlying data, aligning with the
- standard's specifications for secure deletion.
-
- Parameters:
- - file: A file object to be overwritten.
- - file_size: The size of the file in bytes, guiding the overwrite extent.
- """
- try:
- #print("Using: DoD_5220_22_M_wipe")
- for i in range(1, 7):
- # Pass 1, 4, 5: Overwrite with 0x00
- if i in [1, 4, 5]:
- DataManipulation.overwrite_with_pattern(file, b'\x00' * file_size, file_size)
-
- # Pass 2, 6: Overwrite with 0xFF
- if i in [2, 6]:
- DataManipulation.overwrite_with_pattern(file, b'\xFF' * file_size, file_size)
-
- # Pass 3, 7: Overwrite with random data
- if i in [3, 7]:
- random_bytes = bytearray(random.getrandbits(8) for _ in range(file_size))
- DataManipulation.overwrite_with_pattern(file, random_bytes * file_size, file_size)
- DataManipulation.overwrite_with_pattern(file, os.urandom(64), file_size)
- except Exception as e:
- print()
- logging.error(f"Error during DoD 5220.22-M Wipe: {e}")
-
- @staticmethod
- def Schneier_wipe(file, file_size):
- """Overview:
- Adheres to the Schneier wiping protocol, a multi-pass data destruction method. The first two
- passes use fixed byte patterns (0x00 and 0xFF), followed by subsequent passes that introduce
- random data. This sequence is designed to thoroughly scramble the data, enhancing security and
- reducing the possibility of data recovery.
-
- Parameters:
- - file: The file object for wiping.
- - file_size: Size of the file, dictating the wiping process scope.
- """
- try:
- for i in range(1, 7):
- if i in [1]:
- # First pass: overwrite with 0x00
- DataManipulation.overwrite_with_pattern(file, b'\x00' * file_size, file_size)
- if i in [2]:
- # Second pass: overwrite with 0xFF
- DataManipulation.overwrite_with_pattern(file, b'\xFF' * file_size, file_size)
- if i in [3, 4, 5, 6, 7]:
- # Additional passed: overwrite with random data
- random_bytes = bytearray(random.getrandbits(8) for _ in range(file_size))
- DataManipulation.overwrite_with_pattern(file, random_bytes * file_size, file_size)
- DataManipulation.overwrite_with_pattern(file, os.urandom(64), file_size)
- except Exception as e:
- print()
- logging.error(f"Error during Schneier Wipe: {e}")
-
- @staticmethod
- def Gutmann_wipe(file, file_size):
- """Overview:
- Executes the Gutmann wiping method, known for its extensive pattern use. It cycles through
- 35 different patterns, blending predefined and random patterns to overwrite data. This method
- is comprehensive, aiming to address various data remanence possibilities and ensuring a high
- level of data sanitization.
-
- Parameters:
- - file: Target file object for data wiping.
- - file_size: Determines the quantity of data to be overwritten.
- """
- try:
- #print("Gutmann_wipe")
- for i in range(1, 35):
- if i in [1, 2, 3, 4, 32, 33, 34, 35]:
- pattern = bytearray(random.getrandbits(8) for _ in range(file_size)) * file_size
- else:
- patterns = [
- # Passes 5-6
- b"\x55\x55\x55", b"\xAA\xAA\xAA",
- # Passes 7-9
- b"\x92\x49\x24", b"\x49\x24\x92", b"\x24\x92\x49",
- # Passes 10-25
- b"\x00\x00\x00", b"\x11\x11\x11", b"\x22\x22\x22", b"\x33\x33\x33",
- b"\x44\x44\x44", b"\x55\x55\x55", b"\x66\x66\x66", b"\x77\x77\x77",
- b"\x88\x88\x88", b"\x99\x99\x99", b"\xAA\xAA\xAA", b"\xBB\xBB\xBB",
- b"\xCC\xCC\xCC", b"\xDD\xDD\xDD", b"\xEE\xEE\xEE", b"\xFF\xFF\xFF",
- # Passes 26-28
- b"\x92\x49\x24", b"\x49\x24\x92", b"\x24\x92\x49",
- # Passes 29-31
- b"\x6D\xB6\xDB", b"\xB6\xDB\x6D", b"\xDB\x6D\xB6",
- ]
- pattern = patterns[i - 5]
-
- # Calculate repeat_count and the remainder
- pattern_length = len(pattern)
- repeat_count = file_size // pattern_length
- remainder = file_size % pattern_length
-
- # Create the final pattern
- final_pattern = pattern * repeat_count + pattern[:remainder]
- # Overwrite with the final pattern
- DataManipulation.overwrite_with_pattern(file, final_pattern, file_size)
- except Exception as e:
- print()
- logging.error(f"Error during Gutmann Wipe: {e}")
-
- @staticmethod
- def wallet_annihilation(filename, file, data, file_size):
- """Overview:
- This function first encrypts the wallet data, adding a layer of security, and then proceeds
- to a comprehensive wiping process. It utilizes a combination of the DoD 5220.22-M, Schneier,
- and Gutmann methods to ensure thorough overwriting and scrambling of the file data. The
- encryption step is crucial as it secures the data before the wiping begins, making any
- potential recovery attempts even more challenging. The sequence of wiping techniques aims
- to achieve a high degree of certainty that the data cannot be recovered.
-
- Parameters:
- - filename: The name of the file to be wiped.
- - file: File object representing the wallet file.
- - data: The wallet data to be encrypted and then wiped.
- - file_size: The total size of the file, guiding the scope of wiping.
- """
- try:
- # Encryption with SHA3-512 hashes is 100% overkill but we need to ensure that the wallet data is unrecoverable
- hmac_salt = hashlib.sha3_512().hexdigest()
- verification_salt = hashlib.sha3_512().hexdigest()
- password = hashlib.sha3_512().hexdigest()
- verifier = VerificationUtils.hash_password(password, verification_salt)
- totp_secret = TOTP_Utils.generate_totp_secret(True, bytes(verification_salt,'utf-8'))
- encrypted_data = CryptoWallet.encrypt_data(str(data), password, totp_secret, hmac_salt, verification_salt, verifier)
- DataManipulation._save_data(filename, encrypted_data)
- time.sleep(0.5)
-
- random_bytes = bytearray(random.getrandbits(8) for _ in range(file_size))
- DataManipulation.overwrite_with_pattern(file, random_bytes * file_size, file_size)
- time.sleep(0.1)
-
- DataManipulation.DoD_5220_22_M_wipe(file, file_size)
- time.sleep(0.1)
-
- DataManipulation.Schneier_wipe(file, file_size)
- time.sleep(0.1)
-
- DataManipulation.Gutmann_wipe(file, file_size)
- except Exception as e:
- print()
- logging.error(f"Error during Wallet Annihilation: {e}")
-
- @staticmethod
- def delete_wallet(file_path, data, passes=2):
- """Overview:
- This is the main function for securely deleting wallet files. It locks the file to prevent
- concurrent access, then executing the 'wallet_annihilation' method, which combines multiple
- advanced wiping techniques, for each pass specified. The function ensures that the wallet
- file is not just deleted, but it's data is irrecoverably destroyed in the event of too
- many failed password attempts, or overwrite.
-
- Parameters:
- - file_path: The path to the wallet file.
- - data: Data used in the annihilation process.
- - passes: The number of annihilation cycles to execute.
- """
- if not os.path.exists(file_path):
- raise ValueError("File does not exist")
-
- lock = FileLock(file_path+".lock")
- try:
- with lock:
- with open(file_path, "r+b") as file:
- file_size = os.path.getsize(file.name)
- if file_size == 0:
- raise ValueError("File is empty")
-
- for _ in range(passes):
- DataManipulation.wallet_annihilation(file_path, file, data, file_size)
- file.flush()
- os.fsync(file.fileno())
- with open(file_path, "w+b") as file:
- file.truncate(0)
- lock.release()
- except IOError as e:
- print()
- logging.error(f"IOError during file overwrite: {e}")
- except ValueError as e:
- print()
- logging.error(f"ValueError in file handling: {e}")
- except Exception as e:
- print()
- logging.error(f"Unexpected error occurred: {e}")
- finally:
- lock.release()
- try:
- time.sleep(0.5)
- os.remove(file_path)
- if os.path.exists(file_path+".lock"):
- os.remove(file_path+".lock")
- sys.stdout.write("\rWallet Annihilation in progress....")
- print()
- except OSError as e:
- print()
- logging.error(f"Error while removing file: {e}")
-
-class VerificationUtils:
- """
- Handles data verification.
- """
- @staticmethod
- def hash_password(password, salt):
- """
- Generate a cryptographic hash of the password using PBKDF2 and then Scrypt.
- """
- # First layer of hashing using PBKDF2
- salt_bytes = salt
- if not isinstance(salt, bytes):
- salt_bytes = bytes(salt, 'utf-8')
- pbkdf2_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt_bytes, 100000)
-
- # Second layer of hashing using Scrypt
- result = scrypt(pbkdf2_hash, salt=salt, key_len=32, N=2**14, r=8, p=1)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def verify_password(stored_password_hash, provided_password, salt):
- """
- Compares the provided password with the stored hash.
- """
- # Generate hash of the provided password
- verifier = VerificationUtils.hash_password(provided_password, salt)
- # Securely compare the generated hash with the stored hash
- is_verified = hmac_module.compare_digest(verifier, stored_password_hash)
-
- # Nullify verifier if not verified
- if not is_verified:
- verifier = None
- result = is_verified, verifier
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def hmac_util(password=None,hmac_salt=None,stored_hmac=None, hmac_msg=None, verify=False):
- """
- Handle HMAC generation and verification.
- """
- # Generate HMAC key using Scrypt
- hmac_key = scrypt(password.encode(), salt=hmac_salt, key_len=32, N=2**14, r=8, p=1)
- # Generate HMAC of the message
- computed_hmac = hmac_module.new(hmac_key, hmac_msg, hashlib.sha256).digest()
- # If in verify mode, securely compare the computed HMAC with the stored HMAC
- if verify:
- result = hmac_module.compare_digest(computed_hmac, stored_hmac)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
- else:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not computed_hmac])
- return computed_hmac
-
- @staticmethod
- def verify_password_and_hmac(data, password, hmac_salt, verification_salt, deterministic):
- """
- Verifies the given password and HMAC.
-
- Arguments:
- - data: The wallet data
- - password: The user's password
- - hmac_salt: The HMAC salt
-
- Returns:
- - A tuple of booleans indicating if the password and HMAC are verified
- """
- # Decode and verify the stored password verifier
- stored_verifier = base64.b64decode(data["wallet_data"]["verifier"].encode('utf-8'))
- password_verified, _ = VerificationUtils.verify_password(stored_verifier, password, verification_salt)
-
- # Prepare and verify the HMAC message
- hmac_msg = json.dumps(data["wallet_data"]["entry_data"]["entries"]).encode()
- if deterministic:
- hmac_msg += json.dumps(data["wallet_data"]["entry_data"]["key_data"]).encode()
- stored_hmac = base64.b64decode(data["wallet_data"]["hmac"].encode('utf-8'))
- hmac_verified = VerificationUtils.hmac_util(password=password, hmac_salt=hmac_salt, stored_hmac=stored_hmac, hmac_msg=hmac_msg, verify=True)
- result = password_verified, hmac_verified, stored_verifier
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def verify_totp_secret(password,totp_secret,hmac_salt,verification_salt,stored_verifier):
- """
- Validates the given Two-Factor Authentication secret token
- """
- # Decrypt the stored TOTP secret to handle 2FA
- decrypted_totp_secret = CryptoWallet.decrypt_data(totp_secret, password, "", hmac_salt, verification_salt, stored_verifier)
- # Generate a predictable TOTP secret to check against
- predictable_totp_secret = TOTP_Utils.generate_totp_secret(True,verification_salt)
- # If the decrypted TOTP doesn't match the predictable one, handle 2FA validation
- if decrypted_totp_secret != predictable_totp_secret:
- result = decrypted_totp_secret, True
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
- else:
- result = "", False
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def validate_totp_code(secret, code):
- """
- Validates the given Two-Factor Authentication code using the provided secret.
- """
- totp = pyotp.TOTP(secret)
- result = totp.verify(code)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
-class TOTP_Utils:
- @staticmethod
- def generate_totp_secret(predictable, verification_salt):
- """
- Generate a new TOTP secret.
- """
- if not predictable:
- result = pyotp.random_base32()
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
- else:
- result = hashlib.sha256(verification_salt).hexdigest()[:16]
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def generate_totp_code(secret):
- """
- Generate a Two-Factor Authentication code using the given secret.
- """
- totp = pyotp.TOTP(secret)
- result = totp.now()
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
-class CryptoWallet:
- @staticmethod
- def encrypt_data(data, password, totp_secret, hmac_salt, verification_salt, stored_password_hash):
- # 1. Password Verification
- # Verify the provided password against the stored hash and salt
- password_verified, verifier = VerificationUtils.verify_password(stored_password_hash, password, verification_salt)
- if not password_verified and not verifier:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- logging.error("Authentication failed or wallet data is corrupted.")
- raise ValueError("Authentication failed or wallet data is corrupted.")
-
- # 2. AES-GCM Layer Encryption
- # Generate a random 16-byte challenge for AES
- aes_challenge_portion = os.urandom(16)
- # Generate a proof-of-work based on the challenge
- aes_challenge_portion_proof = ProofOfWork.generate_proof(aes_challenge_portion)
- # Scramble the challenge based on the proof-of-work
- scrambled_aes_challenge_portion = DataManipulation.scramble(aes_challenge_portion, aes_challenge_portion_proof)
- # Generate a proof-of-work based on the scrambled challenge
- aes_proof = ProofOfWork.generate_proof(scrambled_aes_challenge_portion)
-
- # Create a commitment by hashing the proof
- aes_commitment = hashlib.sha256(str(aes_proof).encode()).digest()
- # Convert the commitment to a hexadecimal string
- aes_commitment_hex = aes_commitment.hex()
-
- # Scramble all the parameters to be used for key derivation
- scrambled_parameters = [DataManipulation.scramble(param.encode() if isinstance(param, str) else param, aes_proof) for param in [password, totp_secret, aes_commitment_hex, hmac_salt, verifier, verification_salt]]
-
- # Derive AES encryption key using Scrypt
- aes_encryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
- # Generate a random nonce for AES encryption
- aes_nonce = os.urandom(16)
-
- # Scramble and encrypt the data
- scrambled_data = DataManipulation.scramble(data.encode(), aes_proof)
- aes_ct_bytes, aes_tag = EncryptionUtils.aes_gcm_encrypt(scrambled_data, aes_encryption_key, aes_nonce)
-
- # Scramble the ciphertext and tag
- scrambled_aes_ct_bytes = DataManipulation.scramble(aes_ct_bytes, aes_proof)
- scrambled_aes_tag = DataManipulation.scramble(aes_tag, aes_proof)
-
- # Compute HMAC for AES layer
- hmac_1 = VerificationUtils.hmac_util(password=scrambled_parameters[0].decode(), hmac_salt=scrambled_parameters[3], hmac_msg=aes_nonce + scrambled_aes_ct_bytes + scrambled_aes_tag, verify=False)
-
- # 3. ChaCha20-Poly1305 Layer Encryption
- # Generate a random 16-byte challenge for ChaCha20
- chacha_challenge_portion = os.urandom(16)
- # Generate a proof-of-work based on the challenge
- chacha_challenge_portion_proof = ProofOfWork.generate_proof(chacha_challenge_portion)
- # Scramble the challenge based on the proof-of-work
- scrambled_chacha_challenge_portion = DataManipulation.scramble(chacha_challenge_portion, chacha_challenge_portion_proof)
- # Generate a proof-of-work based on the scrambled challenge
- chacha_proof = ProofOfWork.generate_proof(scrambled_chacha_challenge_portion)
-
- # Create a commitment by hashing the proof
- chacha_commitment = hashlib.sha256(str(chacha_proof).encode()).digest()
- # Convert the commitment to a hexadecimal string
- chacha_commitment_hex = chacha_commitment.hex()
-
- # Scramble all the parameters to be used for key derivation
- scrambled_parameters = [DataManipulation.scramble(param.encode() if isinstance(param, str) else param, chacha_proof) for param in [password, totp_secret, chacha_commitment_hex, hmac_salt, verifier, verification_salt]]
-
- # Derive ChaCha20 encryption key using Scrypt
- chacha_encryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
- # Encrypt the data using ChaCha20-Poly1305
- chacha_nonce, chacha_ct_bytes, chacha_tag = EncryptionUtils.chacha20_poly1305_encrypt(aes_challenge_portion + aes_nonce + scrambled_aes_ct_bytes + scrambled_aes_tag + hmac_1, chacha_encryption_key)
-
- # Scramble the ciphertext and tag
- scrambled_chacha_ct_bytes = DataManipulation.scramble(chacha_ct_bytes, chacha_proof)
- scrambled_chacha_tag = DataManipulation.scramble(chacha_tag, chacha_proof)
-
- # Compute HMAC for ChaCha20 layer
- hmac_2 = VerificationUtils.hmac_util(password=chacha_commitment_hex, hmac_salt=scrambled_parameters[3], hmac_msg=chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag, verify=False)
-
- failed_attempts = 0
- failed_attempts_bytes = failed_attempts.to_bytes(4, byteorder='big')
- #print(chacha_proof)
- #print(DataManipulation.scramble(chacha_challenge_portion + chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag + hmac_2, failed_attempts_bytes))
- # Base64 encode the final encrypted data for easier storage and transmission
- result = base64.b64encode(DataManipulation.scramble(chacha_challenge_portion + chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag + hmac_2, failed_attempts_bytes)).decode('utf-8')
-
- # 4. Cleanup and return
- # Securely delete sensitive variables
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
-
- return result
-
- @staticmethod
- def decrypt_data(encrypted_data, password, totp_secret, hmac_salt, verification_salt, stored_password_hash):
- global FAILED_ATTEMPTS, MAX_ATTEMPTS, DIFFICULTY # Global variables for failed attempts and PoW difficulty
-
- # 1. Base64 Decoding
- # Decode the base64 encoded encrypted data
- data = base64.b64decode(encrypted_data.encode('utf-8'))
-
- # 2. Password Verification
- # Verify the provided password against the stored hash and salt
- password_verified, verifier = VerificationUtils.verify_password(stored_password_hash, password, verification_salt)
- if not password_verified and not verifier:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- logging.error("Authentication failed or wallet data is corrupted.")
- raise ValueError("Authentication failed or wallet data is corrupted.")
- else:
- failed_attempts = 0
- failed_attempts_bytes = failed_attempts.to_bytes(4, byteorder='big')
- data = DataManipulation.descramble(data,failed_attempts_bytes)
-
- # 3. ChaCha20-Poly1305 Layer Decryption
- # Update the extraction logic to account for the 4 bytes of failed_attempts
- chacha_challenge_portion = data[:16]
- chacha_nonce = data[16:28]
- scrambled_chacha_ct_bytes = data[28:-48]
- scrambled_chacha_tag = data[-48:-32]
- stored_chacha_hmac = data[-32:]
-
- # Generate a proof-of-work based on the challenge
- chacha_challenge_portion_proof = ProofOfWork.generate_proof(chacha_challenge_portion)
-
- #Check if ChaCha challenge proof is valid
- if not ProofOfWork.is_proof_valid(chacha_challenge_portion_proof, chacha_challenge_portion):
- FAILED_ATTEMPTS += 1
- if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
- #del encrypted_data # Replace with your secure delete function
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- raise ValueError("Too many failed attempts. Data deleted.")
- DIFFICULTY += 1
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- raise ValueError("Invalid ChaCha Challenge proof. Try again.")
-
- # Scramble the challenge based on the proof-of-work
- scrambled_chacha_challenge_portion = DataManipulation.scramble(chacha_challenge_portion, chacha_challenge_portion_proof)
- # Generate another proof-of-work based on the scrambled challenge
- chacha_proof = ProofOfWork.generate_proof(scrambled_chacha_challenge_portion)
-
- #Check if ChaCha scrambled challenge proof is valid
- if not ProofOfWork.is_proof_valid(chacha_proof, scrambled_chacha_challenge_portion):
- FAILED_ATTEMPTS += 1
- if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- #del encrypted_data # Replace with your secure delete function
- raise ValueError("Too many failed attempts. Data deleted.")
- DIFFICULTY += 1
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- raise ValueError("Invalid ChaCha proof. Try again.")
-
- # Compute commitment and convert it to hex
- chacha_commitment_hex = hashlib.sha256(str(chacha_proof).encode()).hexdigest()
-
- # Scramble all parameters for key derivation
- scrambled_parameters = [DataManipulation.scramble(param.encode() if isinstance(param, str) else param, chacha_proof) for param in [password, totp_secret, chacha_commitment_hex, hmac_salt, verifier, verification_salt]]
-
- # Verify HMAC for ChaCha layer data
- if not VerificationUtils.hmac_util(password=chacha_commitment_hex,hmac_salt=scrambled_parameters[3],stored_hmac=stored_chacha_hmac,hmac_msg=chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag,verify=True):
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- logging.error("ChaCha layer data integrity check failed. Wallet data might be corrupted or tampered with.")
- raise ValueError("ChaCha layer data integrity check failed. Wallet data might be corrupted or tampered with.")
-
- # Derive decryption key using Scrypt
- chacha_decryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
-
- # Descramble ChaCha ciphertext and the tag
- chacha_ct_bytes = DataManipulation.descramble(scrambled_chacha_ct_bytes, chacha_proof)
- chacha_tag = DataManipulation.descramble(scrambled_chacha_tag, chacha_proof)
-
- # Decrypt the data using ChaCha20-Poly1305
- chacha_decrypted_data = EncryptionUtils.chacha20_poly1305_decrypt(chacha_nonce, chacha_ct_bytes, chacha_tag, chacha_decryption_key)
-
- # 4. AES-GCM Layer Decryption
- # Extract AES-related portions from the decrypted data
- aes_challenge_portion = chacha_decrypted_data[:16]
- chacha_decrypted_data = chacha_decrypted_data[16:]
- aes_nonce = chacha_decrypted_data[:16]
- scrambled_aes_tag = chacha_decrypted_data[-48:-32]
- stored_aes_hmac = chacha_decrypted_data[-32:]
- scrambled_aes_ct_bytes = chacha_decrypted_data[16:-48]
-
- # Generate a proof-of-work for AES
- aes_challenge_portion_proof = ProofOfWork.generate_proof(aes_challenge_portion)
-
- #Check if AES challenge proof is valid
- if not ProofOfWork.is_proof_valid(aes_challenge_portion_proof, aes_challenge_portion):
- FAILED_ATTEMPTS += 1
- if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- #del encrypted_data # Replace with your secure delete function
- raise ValueError("Too many failed attempts. Data deleted.")
- DIFFICULTY += 1
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- raise ValueError("Invalid AES Challenge proof. Try again.")
-
- # Scramble the AES challenge based on the proof-of-work
- scrambled_aes_challenge_portion = DataManipulation.scramble(aes_challenge_portion, aes_challenge_portion_proof)
- # Generate another proof-of-work based on the scrambled challenge
- aes_proof = ProofOfWork.generate_proof(scrambled_aes_challenge_portion)
-
- #Check if AES scrambled challenge proof is valid
- if not ProofOfWork.is_proof_valid(aes_proof, scrambled_aes_challenge_portion):
- FAILED_ATTEMPTS += 1
- if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- #del encrypted_data # Replace with your secure delete function
- raise ValueError("Too many failed attempts. Data deleted.")
- DIFFICULTY += 1
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- raise ValueError("Invalid AES proof. Try again.")
-
- # Compute commitment and convert it to hex
- aes_commitment_hex = hashlib.sha256(str(aes_proof).encode()).hexdigest()
-
- # Scramble all parameters for key derivation
- scrambled_parameters = [DataManipulation.scramble(param.encode() if isinstance(param, str) else param, aes_proof) for param in [password, totp_secret, aes_commitment_hex, hmac_salt, verifier, verification_salt]]
-
- # Verify HMAC for AES layer data
- if not VerificationUtils.hmac_util(password=scrambled_parameters[0].decode(),hmac_salt=scrambled_parameters[3],stored_hmac=stored_aes_hmac,hmac_msg=aes_nonce + scrambled_aes_ct_bytes + scrambled_aes_tag,verify=True):
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- logging.error("AES layer data integrity check failed. Wallet data might be corrupted or tampered with.")
- raise ValueError("AES layer data integrity check failed. Wallet data might be corrupted or tampered with.")
-
- # Derive decryption key using Scrypt
- aes_decryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
-
- # Descramble the AES ciphertext and tag
- aes_ct_bytes = DataManipulation.descramble(scrambled_aes_ct_bytes, aes_proof)
- aes_tag = DataManipulation.descramble(scrambled_aes_tag,aes_proof)
-
- # Decrypt the data using AES-GCM
- decrypted_data = EncryptionUtils.aes_gcm_decrypt(aes_ct_bytes, aes_tag, aes_decryption_key, aes_nonce)
-
- # Descramble the decrypted data
- decrypted_data = DataManipulation.descramble(decrypted_data, aes_proof)
-
- result = decrypted_data.decode('utf-8')
-
- # 5. Cleanup and return
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def update_failed_attempts(encrypted_data,hmac_salt):
- #encrypted_data = encrypted_data["wallet_data"]["entry_data"]["entries"]
- updated_data = []
-
- #print(f"encrypted_data: {encrypted_data}")
- for encrypted_entry in encrypted_data:
- #print(f"encrypted_entry: {encrypted_entry}")
- data = base64.b64decode(encrypted_entry.encode('utf-8'))
- for n in range(10):
- descrambled_data = DataManipulation.descramble(data,n.to_bytes(4, byteorder='big'))
- chacha_challenge_portion = descrambled_data[:16]
- chacha_nonce = descrambled_data[16:28]
- scrambled_chacha_ct_bytes = descrambled_data[28:-48]
- scrambled_chacha_tag = descrambled_data[-48:-32]
- stored_chacha_hmac = descrambled_data[-32:]
- chacha_challenge_portion_proof = ProofOfWork.generate_proof(chacha_challenge_portion)
- scrambled_chacha_challenge_portion = DataManipulation.scramble(chacha_challenge_portion, chacha_challenge_portion_proof)
- chacha_proof = ProofOfWork.generate_proof(scrambled_chacha_challenge_portion)
- # Create a commitment by hashing the proof
- chacha_commitment = hashlib.sha256(str(chacha_proof).encode()).digest()
- # Convert the commitment to a hexadecimal string
- chacha_commitment_hex = chacha_commitment.hex()
- #print(chacha_proof)
- if VerificationUtils.hmac_util(password=chacha_commitment_hex,hmac_salt=DataManipulation.scramble(hmac_salt,chacha_proof),stored_hmac=stored_chacha_hmac,hmac_msg=chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag,verify=True):
- number_of_attempts = n
- break
- number_of_attempts += + 1
- attempts_left = 10 - number_of_attempts
- rescrambled_data = DataManipulation.scramble(descrambled_data,number_of_attempts.to_bytes(4, byteorder='big'))
- #print(rescrambled_data)
- updated_encrypted_data_base64 = base64.b64encode(rescrambled_data).decode('utf-8')
- updated_data.append(updated_encrypted_data_base64)
- encrypted_data = updated_data
- result = encrypted_data, attempts_left
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
-
- @staticmethod
- def reset_failed_attempts(encrypted_data,hmac_salt):
- #encrypted_data = encrypted_data["wallet_data"]["entry_data"]["entries"]
- updated_data = []
- #print(encrypted_data)
- for encrypted_entry in encrypted_data:
- #print(encrypted_entry)
- data = base64.b64decode(encrypted_entry.encode('utf-8'))
-
- for n in range(10):
- descrambled_data = DataManipulation.descramble(data,n.to_bytes(4, byteorder='big'))
- chacha_challenge_portion = descrambled_data[:16]
- chacha_nonce = descrambled_data[16:28]
- scrambled_chacha_ct_bytes = descrambled_data[28:-48]
- scrambled_chacha_tag = descrambled_data[-48:-32]
- stored_chacha_hmac = descrambled_data[-32:]
- chacha_challenge_portion_proof = ProofOfWork.generate_proof(chacha_challenge_portion)
- scrambled_chacha_challenge_portion = DataManipulation.scramble(chacha_challenge_portion, chacha_challenge_portion_proof)
- chacha_proof = ProofOfWork.generate_proof(scrambled_chacha_challenge_portion)
- # Create a commitment by hashing the proof
- chacha_commitment = hashlib.sha256(str(chacha_proof).encode()).digest()
- # Convert the commitment to a hexadecimal string
- chacha_commitment_hex = chacha_commitment.hex()
- #print(chacha_proof)
- if VerificationUtils.hmac_util(password=chacha_commitment_hex,hmac_salt=DataManipulation.scramble(hmac_salt,chacha_proof),stored_hmac=stored_chacha_hmac,hmac_msg=chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag,verify=True):
- number_of_attempts = n
- break
- number_of_attempts = 0
- rescrambled_data = DataManipulation.scramble(descrambled_data,number_of_attempts.to_bytes(4, byteorder='big'))
- updated_encrypted_data_base64 = base64.b64encode(rescrambled_data).decode('utf-8')
- updated_data.append(updated_encrypted_data_base64)
- encrypted_data = updated_data
- result = encrypted_data, None
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
- return result
\ No newline at end of file
diff --git a/denaro/wallet/utils/cryptographic_util.py b/denaro/wallet/utils/cryptographic_util.py
new file mode 100644
index 0000000..2aab929
--- /dev/null
+++ b/denaro/wallet/utils/cryptographic_util.py
@@ -0,0 +1,450 @@
+import os
+import hashlib
+import pyotp
+from Crypto.Cipher import AES, ChaCha20_Poly1305
+import logging
+import base64
+import data_manipulation_util
+import verification_util
+
+# Global variables
+FAILED_ATTEMPTS = 0
+MAX_ATTEMPTS = 5
+DIFFICULTY = 3
+
+class ProofOfWork:
+ """
+ Handles proof-of-work generation and validation.
+ """
+ @staticmethod
+ def generate_proof(challenge):
+ proof = 0
+ target = "1" * DIFFICULTY
+ while not hashlib.sha256(challenge + str(proof).encode()).hexdigest().startswith(target):
+ proof += 1
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not proof])
+ return proof
+
+ @staticmethod
+ def is_proof_valid(proof, challenge):
+ result = hashlib.sha256(challenge + str(proof).encode()).hexdigest().startswith("1" * DIFFICULTY)
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+class TOTP:
+ @staticmethod
+ def generate_totp_secret(predictable, verification_salt):
+ """
+ Generate a new TOTP secret.
+ """
+ if not predictable:
+ result = pyotp.random_base32()
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+ else:
+ result = hashlib.sha256(verification_salt).hexdigest()[:16]
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def generate_totp_code(secret):
+ """
+ Generate a Two-Factor Authentication code using the given secret.
+ """
+ totp = pyotp.TOTP(secret)
+ result = totp.now()
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+class EncryptDecryptUtils:
+ """
+ Handles encryption and decryption tasks.
+ """
+ @staticmethod
+ def aes_gcm_encrypt(data, key, nonce):
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
+ ciphertext, tag = cipher.encrypt_and_digest(data)
+ result = ciphertext, tag
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def aes_gcm_decrypt(ciphertext, tag, key, nonce):
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
+ result = cipher.decrypt_and_verify(ciphertext, tag)
+ return result
+
+ @staticmethod
+ def chacha20_poly1305_encrypt(data, key):
+ cipher = ChaCha20_Poly1305.new(key=key)
+ ciphertext, tag = cipher.encrypt_and_digest(data)
+ result = cipher.nonce, ciphertext, tag
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def chacha20_poly1305_decrypt(nonce, ciphertext, tag, decryption_key):
+ """
+ Decrypt data using ChaCha20-Poly1305.
+ """
+ cipher = ChaCha20_Poly1305.new(key=decryption_key, nonce=nonce)
+ decrypted_data = cipher.decrypt(ciphertext)
+ try:
+ cipher.verify(tag)
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not decrypted_data])
+ return decrypted_data
+ except ValueError:
+ logging.error("ChaCha20-Poly1305 tag verification failed. Data might be corrupted or tampered with.")
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ raise ValueError("ChaCha20-Poly1305 tag verification failed. Data might be corrupted or tampered with.")
+
+ @staticmethod
+ def encrypt_data(data, password, totp_secret, hmac_salt, verification_salt, stored_password_hash):
+ # 1. Password Verification
+ # Verify the provided password against the stored hash and salt
+ password_verified, verifier = verification_util.Verification.verify_password(stored_password_hash, password, verification_salt)
+ if not password_verified and not verifier:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ logging.error("Authentication failed or wallet data is corrupted.")
+ raise ValueError("Authentication failed or wallet data is corrupted.")
+
+ # 2. AES-GCM Layer Encryption
+ # Generate a random 16-byte challenge for AES
+ aes_challenge_portion = os.urandom(16)
+ # Generate a proof-of-work based on the challenge
+ aes_challenge_portion_proof = ProofOfWork.generate_proof(aes_challenge_portion)
+ # Scramble the challenge based on the proof-of-work
+ scrambled_aes_challenge_portion = data_manipulation_util.DataManipulation.scramble(aes_challenge_portion, aes_challenge_portion_proof)
+ # Generate a proof-of-work based on the scrambled challenge
+ aes_proof = ProofOfWork.generate_proof(scrambled_aes_challenge_portion)
+
+ # Create a commitment by hashing the proof
+ aes_commitment = hashlib.sha256(str(aes_proof).encode()).digest()
+ # Convert the commitment to a hexadecimal string
+ aes_commitment_hex = aes_commitment.hex()
+
+ # Scramble all the parameters to be used for key derivation
+ scrambled_parameters = [data_manipulation_util.DataManipulation.scramble(param.encode() if isinstance(param, str) else param, aes_proof) for param in [password, totp_secret, aes_commitment_hex, hmac_salt, verifier, verification_salt]]
+
+ # Derive AES encryption key using Scrypt
+ aes_encryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
+ # Generate a random nonce for AES encryption
+ aes_nonce = os.urandom(16)
+
+ # Scramble and encrypt the data
+ scrambled_data = data_manipulation_util.DataManipulation.scramble(data.encode(), aes_proof)
+ aes_ct_bytes, aes_tag = EncryptDecryptUtils.aes_gcm_encrypt(scrambled_data, aes_encryption_key, aes_nonce)
+
+ # Scramble the ciphertext and tag
+ scrambled_aes_ct_bytes = data_manipulation_util.DataManipulation.scramble(aes_ct_bytes, aes_proof)
+ scrambled_aes_tag = data_manipulation_util.DataManipulation.scramble(aes_tag, aes_proof)
+
+ # Compute HMAC for AES layer
+ hmac_1 = verification_util.Verification.hmac_util(password=scrambled_parameters[0].decode(), hmac_salt=scrambled_parameters[3], hmac_msg=aes_nonce + scrambled_aes_ct_bytes + scrambled_aes_tag, verify=False)
+
+ # 3. ChaCha20-Poly1305 Layer Encryption
+ # Generate a random 16-byte challenge for ChaCha20
+ chacha_challenge_portion = os.urandom(16)
+ # Generate a proof-of-work based on the challenge
+ chacha_challenge_portion_proof = ProofOfWork.generate_proof(chacha_challenge_portion)
+ # Scramble the challenge based on the proof-of-work
+ scrambled_chacha_challenge_portion = data_manipulation_util.DataManipulation.scramble(chacha_challenge_portion, chacha_challenge_portion_proof)
+ # Generate a proof-of-work based on the scrambled challenge
+ chacha_proof = ProofOfWork.generate_proof(scrambled_chacha_challenge_portion)
+
+ # Create a commitment by hashing the proof
+ chacha_commitment = hashlib.sha256(str(chacha_proof).encode()).digest()
+ # Convert the commitment to a hexadecimal string
+ chacha_commitment_hex = chacha_commitment.hex()
+
+ # Scramble all the parameters to be used for key derivation
+ scrambled_parameters = [data_manipulation_util.DataManipulation.scramble(param.encode() if isinstance(param, str) else param, chacha_proof) for param in [password, totp_secret, chacha_commitment_hex, hmac_salt, verifier, verification_salt]]
+
+ # Derive ChaCha20 encryption key using Scrypt
+ chacha_encryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
+ # Encrypt the data using ChaCha20-Poly1305
+ chacha_nonce, chacha_ct_bytes, chacha_tag = EncryptDecryptUtils.chacha20_poly1305_encrypt(aes_challenge_portion + aes_nonce + scrambled_aes_ct_bytes + scrambled_aes_tag + hmac_1, chacha_encryption_key)
+
+ # Scramble the ciphertext and tag
+ scrambled_chacha_ct_bytes = data_manipulation_util.DataManipulation.scramble(chacha_ct_bytes, chacha_proof)
+ scrambled_chacha_tag = data_manipulation_util.DataManipulation.scramble(chacha_tag, chacha_proof)
+
+ # Compute HMAC for ChaCha20 layer
+ hmac_2 = verification_util.Verification.hmac_util(password=chacha_commitment_hex, hmac_salt=scrambled_parameters[3], hmac_msg=chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag, verify=False)
+
+ failed_attempts = 0
+ failed_attempts_bytes = failed_attempts.to_bytes(4, byteorder='big')
+ #print(chacha_proof)
+ #print(data_manipulation_util.DataManipulation.scramble(chacha_challenge_portion + chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag + hmac_2, failed_attempts_bytes))
+ # Base64 encode the final encrypted data for easier storage and transmission
+ result = base64.b64encode(data_manipulation_util.DataManipulation.scramble(chacha_challenge_portion + chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag + hmac_2, failed_attempts_bytes)).decode('utf-8')
+
+ # 4. Cleanup and return
+ # Securely delete sensitive variables
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+
+ return result
+
+ @staticmethod
+ def decrypt_data(encrypted_data, password, totp_secret, hmac_salt, verification_salt, stored_password_hash):
+ global FAILED_ATTEMPTS, MAX_ATTEMPTS, DIFFICULTY # Global variables for failed attempts and PoW difficulty
+
+ # 1. Base64 Decoding
+ # Decode the base64 encoded encrypted data
+ data = base64.b64decode(encrypted_data.encode('utf-8'))
+
+ # 2. Password Verification
+ # Verify the provided password against the stored hash and salt
+ password_verified, verifier = verification_util.Verification.verify_password(stored_password_hash, password, verification_salt)
+ if not password_verified and not verifier:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ logging.error("Authentication failed or wallet data is corrupted.")
+ raise ValueError("Authentication failed or wallet data is corrupted.")
+ else:
+ failed_attempts = 0
+ failed_attempts_bytes = failed_attempts.to_bytes(4, byteorder='big')
+ data = data_manipulation_util.DataManipulation.descramble(data,failed_attempts_bytes)
+
+ # 3. ChaCha20-Poly1305 Layer Decryption
+ # Update the extraction logic to account for the 4 bytes of failed_attempts
+ chacha_challenge_portion = data[:16]
+ chacha_nonce = data[16:28]
+ scrambled_chacha_ct_bytes = data[28:-48]
+ scrambled_chacha_tag = data[-48:-32]
+ stored_chacha_hmac = data[-32:]
+
+ # Generate a proof-of-work based on the challenge
+ chacha_challenge_portion_proof = ProofOfWork.generate_proof(chacha_challenge_portion)
+
+ #Check if ChaCha challenge proof is valid
+ if not ProofOfWork.is_proof_valid(chacha_challenge_portion_proof, chacha_challenge_portion):
+ FAILED_ATTEMPTS += 1
+ if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
+ #del encrypted_data # Replace with your secure delete function
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ raise ValueError("Too many failed attempts. Data deleted.")
+ DIFFICULTY += 1
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ raise ValueError("Invalid ChaCha Challenge proof. Try again.")
+
+ # Scramble the challenge based on the proof-of-work
+ scrambled_chacha_challenge_portion = data_manipulation_util.DataManipulation.scramble(chacha_challenge_portion, chacha_challenge_portion_proof)
+ # Generate another proof-of-work based on the scrambled challenge
+ chacha_proof = ProofOfWork.generate_proof(scrambled_chacha_challenge_portion)
+
+ #Check if ChaCha scrambled challenge proof is valid
+ if not ProofOfWork.is_proof_valid(chacha_proof, scrambled_chacha_challenge_portion):
+ FAILED_ATTEMPTS += 1
+ if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ #del encrypted_data # Replace with your secure delete function
+ raise ValueError("Too many failed attempts. Data deleted.")
+ DIFFICULTY += 1
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ raise ValueError("Invalid ChaCha proof. Try again.")
+
+ # Compute commitment and convert it to hex
+ chacha_commitment_hex = hashlib.sha256(str(chacha_proof).encode()).hexdigest()
+
+ # Scramble all parameters for key derivation
+ scrambled_parameters = [data_manipulation_util.DataManipulation.scramble(param.encode() if isinstance(param, str) else param, chacha_proof) for param in [password, totp_secret, chacha_commitment_hex, hmac_salt, verifier, verification_salt]]
+
+ # Verify HMAC for ChaCha layer data
+ if not verification_util.Verification.hmac_util(password=chacha_commitment_hex,hmac_salt=scrambled_parameters[3],stored_hmac=stored_chacha_hmac,hmac_msg=chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag,verify=True):
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ logging.error("ChaCha layer data integrity check failed. Wallet data might be corrupted or tampered with.")
+ raise ValueError("ChaCha layer data integrity check failed. Wallet data might be corrupted or tampered with.")
+
+ # Derive decryption key using Scrypt
+ chacha_decryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
+
+ # Descramble ChaCha ciphertext and the tag
+ chacha_ct_bytes = data_manipulation_util.DataManipulation.descramble(scrambled_chacha_ct_bytes, chacha_proof)
+ chacha_tag = data_manipulation_util.DataManipulation.descramble(scrambled_chacha_tag, chacha_proof)
+
+ # Decrypt the data using ChaCha20-Poly1305
+ chacha_decrypted_data = EncryptDecryptUtils.chacha20_poly1305_decrypt(chacha_nonce, chacha_ct_bytes, chacha_tag, chacha_decryption_key)
+
+ # 4. AES-GCM Layer Decryption
+ # Extract AES-related portions from the decrypted data
+ aes_challenge_portion = chacha_decrypted_data[:16]
+ chacha_decrypted_data = chacha_decrypted_data[16:]
+ aes_nonce = chacha_decrypted_data[:16]
+ scrambled_aes_tag = chacha_decrypted_data[-48:-32]
+ stored_aes_hmac = chacha_decrypted_data[-32:]
+ scrambled_aes_ct_bytes = chacha_decrypted_data[16:-48]
+
+ # Generate a proof-of-work for AES
+ aes_challenge_portion_proof = ProofOfWork.generate_proof(aes_challenge_portion)
+
+ #Check if AES challenge proof is valid
+ if not ProofOfWork.is_proof_valid(aes_challenge_portion_proof, aes_challenge_portion):
+ FAILED_ATTEMPTS += 1
+ if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ #del encrypted_data # Replace with your secure delete function
+ raise ValueError("Too many failed attempts. Data deleted.")
+ DIFFICULTY += 1
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ raise ValueError("Invalid AES Challenge proof. Try again.")
+
+ # Scramble the AES challenge based on the proof-of-work
+ scrambled_aes_challenge_portion = data_manipulation_util.DataManipulation.scramble(aes_challenge_portion, aes_challenge_portion_proof)
+ # Generate another proof-of-work based on the scrambled challenge
+ aes_proof = ProofOfWork.generate_proof(scrambled_aes_challenge_portion)
+
+ #Check if AES scrambled challenge proof is valid
+ if not ProofOfWork.is_proof_valid(aes_proof, scrambled_aes_challenge_portion):
+ FAILED_ATTEMPTS += 1
+ if FAILED_ATTEMPTS >= MAX_ATTEMPTS:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ #del encrypted_data # Replace with your secure delete function
+ raise ValueError("Too many failed attempts. Data deleted.")
+ DIFFICULTY += 1
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ raise ValueError("Invalid AES proof. Try again.")
+
+ # Compute commitment and convert it to hex
+ aes_commitment_hex = hashlib.sha256(str(aes_proof).encode()).hexdigest()
+
+ # Scramble all parameters for key derivation
+ scrambled_parameters = [data_manipulation_util.DataManipulation.scramble(param.encode() if isinstance(param, str) else param, aes_proof) for param in [password, totp_secret, aes_commitment_hex, hmac_salt, verifier, verification_salt]]
+
+ # Verify HMAC for AES layer data
+ if not verification_util.Verification.hmac_util(password=scrambled_parameters[0].decode(),hmac_salt=scrambled_parameters[3],stored_hmac=stored_aes_hmac,hmac_msg=aes_nonce + scrambled_aes_ct_bytes + scrambled_aes_tag,verify=True):
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ logging.error("AES layer data integrity check failed. Wallet data might be corrupted or tampered with.")
+ raise ValueError("AES layer data integrity check failed. Wallet data might be corrupted or tampered with.")
+
+ # Derive decryption key using Scrypt
+ aes_decryption_key = hashlib.scrypt(b''.join(scrambled_parameters), salt=scrambled_parameters[-1], n=2**14, r=8, p=1, dklen=32)
+
+ # Descramble the AES ciphertext and tag
+ aes_ct_bytes = data_manipulation_util.DataManipulation.descramble(scrambled_aes_ct_bytes, aes_proof)
+ aes_tag = data_manipulation_util.DataManipulation.descramble(scrambled_aes_tag,aes_proof)
+
+ # Decrypt the data using AES-GCM
+ decrypted_data = EncryptDecryptUtils.aes_gcm_decrypt(aes_ct_bytes, aes_tag, aes_decryption_key, aes_nonce)
+
+ # Descramble the decrypted data
+ decrypted_data = data_manipulation_util.DataManipulation.descramble(decrypted_data, aes_proof)
+
+ result = decrypted_data.decode('utf-8')
+
+ # 5. Cleanup and return
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def get_failed_attempts(data, hmac_salt):
+ """
+ Overview:
+ This method is designed to ascertain the number of unsuccessful password attempts encoded within encrypted data.
+ It sequentially descrambles the data using varying nonce values derived from the range 1-10, extracts several
+ cryptographic segments (like ChaCha20 components and HMAC), and engages in a proof-of-work challenge-response cycle.
+ This cycle includes scrambling and descrambling, along with HMAC verification using the provided salt.
+ The loop terminates upon successful HMAC validation, reflecting the correct attempt count.
+
+ Parameters:
+ - data (bytes): The encrypted data containing encoded information about password attempt failures.
+ - hmac_salt (bytes): The cryptographic salt utilized in the HMAC verification process for enhanced security.
+
+ Returns:
+ Tuple (bytes, int):
+ A tuple comprising the descrambled data and the determined number of failed password attempts.
+ """
+ # Loop between 1-10 to get amount of failed password attempts
+ for n in range(10):
+ # Descrample Data
+ descrambled_data = data_manipulation_util.DataManipulation.descramble(data,n.to_bytes(4, byteorder='big'))
+ # Extract cryptographic data
+ chacha_challenge_portion = descrambled_data[:16]
+ chacha_nonce = descrambled_data[16:28]
+ scrambled_chacha_ct_bytes = descrambled_data[28:-48]
+ scrambled_chacha_tag = descrambled_data[-48:-32]
+ stored_chacha_hmac = descrambled_data[-32:]
+ # Handle proof of work
+ chacha_challenge_portion_proof = ProofOfWork.generate_proof(chacha_challenge_portion)
+ scrambled_chacha_challenge_portion = data_manipulation_util.DataManipulation.scramble(chacha_challenge_portion, chacha_challenge_portion_proof)
+ chacha_proof = ProofOfWork.generate_proof(scrambled_chacha_challenge_portion)
+ # Create a commitment by hashing the proof
+ chacha_commitment = hashlib.sha256(str(chacha_proof).encode()).digest()
+ # Convert the commitment to a hexadecimal string
+ chacha_commitment_hex = chacha_commitment.hex()
+ # Verify HMAC of data within loop iteration
+ if verification_util.Verification.hmac_util(password=chacha_commitment_hex,hmac_salt=data_manipulation_util.DataManipulation.scramble(hmac_salt,chacha_proof),stored_hmac=stored_chacha_hmac,hmac_msg=chacha_nonce + scrambled_chacha_ct_bytes + scrambled_chacha_tag,verify=True):
+ # If the HMAC is verified, set number_of_attempts to n and break loop
+ number_of_attempts = n
+ break
+ # Return the nessessary data
+ return descrambled_data, number_of_attempts
+
+ @staticmethod
+ def update_failed_attempts(encrypted_data, hmac_salt):
+ """
+ Overview:
+ This method updates the count of failed password attempts for each encrypted data entry. It iterates through
+ a list of encrypted entries, decodes them from base64, and employs 'get_failed_attempts' to retrieve the current
+ count of failed attempts. Each count is then incremented, signifying an additional failed attempt. The data is then
+ rescrambled with the incremented count and re-encoded in base64. Afterwhich, the nessessary data is updated and returned.
+
+ Parameters:
+ - encrypted_data (list of strings): A collection of base64 encoded strings representing encrypted data entries.
+ - hmac_salt (bytes): The cryptographic salt used in HMAC operations for data authentication.
+
+ Returns:
+ Tuple (list of strings, int):
+ The updated list of encrypted data entries in base64 format with revised attempt counts,
+ alongside the remaining number of attempts before breach protocol activation.
+ """
+ #encrypted_data = encrypted_data["wallet_data"]["entry_data"]["entries"]
+ updated_data = []
+ #print(f"encrypted_data: {encrypted_data}")
+ for encrypted_entry in encrypted_data:
+ #print(f"encrypted_entry: {encrypted_entry}")
+ data = base64.b64decode(encrypted_entry.encode('utf-8'))
+ descrambled_data, number_of_attempts = EncryptDecryptUtils.get_failed_attempts(data, hmac_salt)
+ number_of_attempts += + 1
+ attempts_left = 10 - number_of_attempts
+ rescrambled_data = data_manipulation_util.DataManipulation.scramble(descrambled_data, number_of_attempts.to_bytes(4, byteorder='big'))
+ #print(rescrambled_data)
+ updated_encrypted_data_base64 = base64.b64encode(rescrambled_data).decode('utf-8')
+ updated_data.append(updated_encrypted_data_base64)
+ encrypted_data = updated_data
+ result = encrypted_data, attempts_left
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def reset_failed_attempts(encrypted_data, hmac_salt):
+ """
+ Overview:
+ This method resets the failed password attempt count for each entry in a set of encrypted data.
+ By utilizing 'get_failed_attempts', it fetches the current attempt count, then resets this to zero,
+ reflecting a revalidated access. The data is then rescrambled with the new count and re-encoded in base64.
+ Afterwhich, the nessessary data is updated and returned.
+
+ Parameters:
+ - encrypted_data (list of strings): A series of base64 encoded strings that represent encrypted data entries.
+ - hmac_salt (bytes): Salt used in HMAC for ensuring data authenticity and integrity.
+
+ Returns:
+ Tuple (list of strings, NoneType):
+ A list of updated encrypted entries with reset attempt counts, encoded in base64,
+ and a None value, signifying no secondary return value.
+ """
+ #encrypted_data = encrypted_data["wallet_data"]["entry_data"]["entries"]
+ updated_data = []
+ #print(encrypted_data)
+ for encrypted_entry in encrypted_data:
+ #print(encrypted_entry)
+ data = base64.b64decode(encrypted_entry.encode('utf-8'))
+ descrambled_data, number_of_attempts = EncryptDecryptUtils.get_failed_attempts(data, hmac_salt)
+ number_of_attempts = 0
+ rescrambled_data = data_manipulation_util.DataManipulation.scramble(descrambled_data,number_of_attempts.to_bytes(4, byteorder='big'))
+ updated_encrypted_data_base64 = base64.b64encode(rescrambled_data).decode('utf-8')
+ updated_data.append(updated_encrypted_data_base64)
+ encrypted_data = updated_data
+ result = encrypted_data, None
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
\ No newline at end of file
diff --git a/denaro/wallet/utils/data_manipulation_util.py b/denaro/wallet/utils/data_manipulation_util.py
new file mode 100644
index 0000000..14544cb
--- /dev/null
+++ b/denaro/wallet/utils/data_manipulation_util.py
@@ -0,0 +1,423 @@
+import os
+import sys
+import logging
+import hashlib
+import random
+import time
+import ctypes
+import json
+from filelock import FileLock
+import cryptographic_util
+import verification_util
+
+class DataManipulation:
+ """
+ Handles data scrambling and descrambling.
+ """
+ dot_count = 0
+ iteration_count = 0
+
+ @staticmethod
+ def scramble(data, seed):
+ if isinstance(seed, int):
+ seed = seed.to_bytes((seed.bit_length() + 7) // 8, 'big')
+ random.seed(hashlib.sha256(seed).digest())
+ indices = list(range(len(data)))
+ random.shuffle(indices)
+ scrambled_data = bytearray(len(data))
+ for i, j in enumerate(indices):
+ scrambled_data[j] = data[i]
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not scrambled_data])
+ return scrambled_data
+
+ @staticmethod
+ def descramble(scrambled_data, seed):
+ if isinstance(seed, int):
+ seed = seed.to_bytes((seed.bit_length() + 7) // 8, 'big')
+ random.seed(hashlib.sha256(seed).digest())
+ indices = list(range(len(scrambled_data)))
+ random.shuffle(indices)
+ data = bytearray(len(scrambled_data))
+ for i, j in enumerate(indices):
+ data[i] = scrambled_data[j]
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not data])
+ return data
+
+ @staticmethod
+ def update_or_reset_attempts(data, filename, hmac_salt, password_verified, deterministic):
+ """
+ Updates or resets failed login attempts based on whether the password was verified.
+
+ Arguments:
+ - data: The wallet data
+ - hmac_salt: The HMAC salt
+ - password_verified: Boolean indicating if the password is verified
+ - filename: The name of the wallet file
+ - deterministic: Boolean indicating if the wallet is deterministic
+ """
+ # Determine the appropriate function to update or reset attempts
+ update_or_reset = cryptographic_util.EncryptDecryptUtils.update_failed_attempts if not password_verified else cryptographic_util.EncryptDecryptUtils.reset_failed_attempts
+
+ # Define keys to update or reset based on deterministic flag
+ key_list = [["entry_data", "entries"],["totp_secret"]]
+ if "imported_entries" in data["wallet_data"]["entry_data"]:
+ key_list.append(["entry_data", "imported_entries"])
+ #key_list.append(["entry_data", "imported_entries"])
+
+ if deterministic:
+ key_list.append(["entry_data", "key_data"])
+
+ #key_list.append(["totp_secret"])
+
+ # Update or reset the attempts for each key in the wallet data
+ for key in key_list:
+ # Initialize target_data as the root dictionary
+ target_data = data["wallet_data"]
+ for k in key:
+ # Navigate through nested keys
+ target_data = target_data.get(k, {})
+
+ # Convert to list if target_data is not a list
+ if not isinstance(target_data, list):
+ target_data = [target_data]
+
+ # Convert each entry to string
+ target_data = [str(entry) for entry in target_data]
+
+ # Update or reset attempts
+ updated_data, attempts_left = update_or_reset(target_data, hmac_salt)
+
+ # Save the updated data back into the original data structure
+ if len(key) == 1:
+ data["wallet_data"][key[0]] = updated_data
+ else:
+ data["wallet_data"][key[0]][key[1]] = updated_data
+
+ data["wallet_data"]["totp_secret"] = data["wallet_data"]["totp_secret"][0]
+ if not password_verified:
+ if attempts_left:
+ print(f"\nPassword Attempts Left: {attempts_left}")
+ if attempts_left <= 5 and attempts_left > 3 and attempts_left != 0:
+ logging.warning(f"Password attempts left are approaching 0, after which any existing wallet data will be ERASED AND POTENTIALLY UNRECOVERABLE!!")
+ if attempts_left <= 3 and attempts_left != 0:
+ logging.critical(f"PASSWORD ATTEMPTS LEFT ARE APPROACHING 0, AFTER WHICH ANY EXISTING WALLET DATA WILL BE ERASED AND POTENTIALLY UNRECOVERABLE!!")
+ if attempts_left == 0:
+ print(f"\nPassword Attempts Left: {attempts_left}")
+ DataManipulation.delete_wallet(filename, data)
+ print("Wallet data has been permanetly erased.")
+ time.sleep(0.5)
+ data = None
+
+ # Securely delete sensitive variables
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not data])
+ return data
+
+ @staticmethod
+ def secure_delete(var):
+ """Overview:
+ This function aims to securely delete a variable by overwriting its memory footprint with zeros, thus ensuring
+ that sensitive data does not linger in memory. By employing both native Python techniques and lower-level memory
+ operations, it ensures that sensitive data remnants are minimized, reducing exposure to potential threats.
+
+ Arguments:
+ - var (various types): The variable which needs to be securely deleted and its memory overwritten.
+
+ Returns:
+ - None: This function works by side-effect, modifying memory directly.
+ """
+ try:
+ # Attempt to get the memory size of the variable using ctypes
+ var_size = ctypes.sizeof(var)
+ # Create a byte array of zeros with the size of the variable
+ zeros = (ctypes.c_byte * var_size)()
+ # Retrieve the memory address of the variable
+ var_address = id(var)
+ # Overwrite the variable's memory location with zeros
+ ctypes.memmove(var_address, zeros, var_size)
+ except TypeError:
+ # Handle different types of variables
+ if isinstance(var, (str, bytes)):
+ # For immutable types, reassigning is the only way, but it's not 100% secure
+ var = '0' * len(var)
+ elif isinstance(var, list):
+ # For lists, we can zero out each element
+ for i in range(len(var)):
+ var[i] = 0
+ elif isinstance(var, dict):
+ # For dictionaries, set each value to zero
+ for key in var:
+ var[key] = 0
+ else:
+ # For other unsupported types, just reassign to zero
+ var = 0
+ finally:
+ # Explicitly delete the variable reference
+ del var
+
+ @staticmethod
+ def _save_data(filename, data):
+ """
+ Persistently stores wallet data to a specified file.
+ """
+ try:
+ with open(filename, 'w') as f:
+ if data:
+ json.dump(data, f, indent=4)
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ else:
+ f = data
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ except Exception as e:
+ logging.error(f"Error saving data to file: {str(e)}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+
+ @staticmethod
+ def overwrite_with_pattern(file, pattern, file_size):
+ """Overview:
+ This function is designed for secure file overwriting. It methodically replaces the content
+ of a file with a given pattern. It continuously writes the pattern to the file, ensuring complete
+ coverage of the original data. The function also includes progress logging and strict data integrity
+ measures, like buffer flushing and disk state synchronization.
+
+ Parameters:
+ - file: A binary-write-mode file object.
+ - pattern: Byte string used as the overwrite pattern.
+ - file_size: Total size of the file to be overwritten, in bytes.
+ """
+ try:
+ file.seek(0) # Start at the beginning of the file
+ bytes_written = 0
+
+ # Update interval for printing progress, can be adjusted for efficiency
+ update_interval = max(1, file_size // 1)
+
+ while bytes_written < file_size:
+ write_size = min(file_size - bytes_written, len(pattern))
+ file.write(pattern[:write_size])
+ bytes_written += write_size
+
+ # Update progress at intervals
+ if bytes_written % update_interval == 0 or bytes_written == file_size:
+ DataManipulation.dot_count += 1
+ if DataManipulation.dot_count >= 4:
+ DataManipulation.dot_count = 0
+ DataManipulation.iteration_count += 1
+ if DataManipulation.iteration_count > 4:
+ DataManipulation.iteration_count = 1
+ sys.stdout.write("\r" + " " * 50) # Clear with 50 spaces
+ sys.stdout.write("\rWallet Annihilation in progress" + "." * DataManipulation.iteration_count)
+ sys.stdout.flush()
+
+ file.flush()
+ os.fsync(file.fileno())
+ except IOError as e:
+ print()
+ logging.error(f"IOError during file overwrite: {e}")
+ except Exception as e:
+ print()
+ logging.error(f"Unexpected error during file overwrite: {e}")
+
+ @staticmethod
+ def DoD_5220_22_M_wipe(file, file_size):
+ """Overview:
+ Implements the DoD 5220.22-M wiping standard, a recognized method for secure file erasure.
+ This function executes multiple overwrite passes, using a mix of zero bytes, one bytes, and
+ random data. Each pass serves to further obfuscate the underlying data, aligning with the
+ standard's specifications for secure deletion.
+
+ Parameters:
+ - file: A file object to be overwritten.
+ - file_size: The size of the file in bytes, guiding the overwrite extent.
+ """
+ try:
+ #print("Using: DoD_5220_22_M_wipe")
+ for i in range(1, 7):
+ # Pass 1, 4, 5: Overwrite with 0x00
+ if i in [1, 4, 5]:
+ DataManipulation.overwrite_with_pattern(file, b'\x00' * file_size, file_size)
+
+ # Pass 2, 6: Overwrite with 0xFF
+ if i in [2, 6]:
+ DataManipulation.overwrite_with_pattern(file, b'\xFF' * file_size, file_size)
+
+ # Pass 3, 7: Overwrite with random data
+ if i in [3, 7]:
+ random_bytes = bytearray(random.getrandbits(8) for _ in range(file_size))
+ DataManipulation.overwrite_with_pattern(file, random_bytes * file_size, file_size)
+ DataManipulation.overwrite_with_pattern(file, os.urandom(64), file_size)
+ except Exception as e:
+ print()
+ logging.error(f"Error during DoD 5220.22-M Wipe: {e}")
+
+ @staticmethod
+ def Schneier_wipe(file, file_size):
+ """Overview:
+ Adheres to the Schneier wiping protocol, a multi-pass data destruction method. The first two
+ passes use fixed byte patterns (0x00 and 0xFF), followed by subsequent passes that introduce
+ random data. This sequence is designed to thoroughly scramble the data, enhancing security and
+ reducing the possibility of data recovery.
+
+ Parameters:
+ - file: The file object for wiping.
+ - file_size: Size of the file, dictating the wiping process scope.
+ """
+ try:
+ for i in range(1, 7):
+ if i in [1]:
+ # First pass: overwrite with 0x00
+ DataManipulation.overwrite_with_pattern(file, b'\x00' * file_size, file_size)
+ if i in [2]:
+ # Second pass: overwrite with 0xFF
+ DataManipulation.overwrite_with_pattern(file, b'\xFF' * file_size, file_size)
+ if i in [3, 4, 5, 6, 7]:
+ # Additional passed: overwrite with random data
+ random_bytes = bytearray(random.getrandbits(8) for _ in range(file_size))
+ DataManipulation.overwrite_with_pattern(file, random_bytes * file_size, file_size)
+ DataManipulation.overwrite_with_pattern(file, os.urandom(64), file_size)
+ except Exception as e:
+ print()
+ logging.error(f"Error during Schneier Wipe: {e}")
+
+ @staticmethod
+ def Gutmann_wipe(file, file_size):
+ """Overview:
+ Executes the Gutmann wiping method, known for its extensive pattern use. It cycles through
+ 35 different patterns, blending predefined and random patterns to overwrite data. This method
+ is comprehensive, aiming to address various data remanence possibilities and ensuring a high
+ level of data sanitization.
+
+ Parameters:
+ - file: Target file object for data wiping.
+ - file_size: Determines the quantity of data to be overwritten.
+ """
+ try:
+ #print("Gutmann_wipe")
+ for i in range(1, 35):
+ if i in [1, 2, 3, 4, 32, 33, 34, 35]:
+ pattern = bytearray(random.getrandbits(8) for _ in range(file_size)) * file_size
+ else:
+ patterns = [
+ # Passes 5-6
+ b"\x55\x55\x55", b"\xAA\xAA\xAA",
+ # Passes 7-9
+ b"\x92\x49\x24", b"\x49\x24\x92", b"\x24\x92\x49",
+ # Passes 10-25
+ b"\x00\x00\x00", b"\x11\x11\x11", b"\x22\x22\x22", b"\x33\x33\x33",
+ b"\x44\x44\x44", b"\x55\x55\x55", b"\x66\x66\x66", b"\x77\x77\x77",
+ b"\x88\x88\x88", b"\x99\x99\x99", b"\xAA\xAA\xAA", b"\xBB\xBB\xBB",
+ b"\xCC\xCC\xCC", b"\xDD\xDD\xDD", b"\xEE\xEE\xEE", b"\xFF\xFF\xFF",
+ # Passes 26-28
+ b"\x92\x49\x24", b"\x49\x24\x92", b"\x24\x92\x49",
+ # Passes 29-31
+ b"\x6D\xB6\xDB", b"\xB6\xDB\x6D", b"\xDB\x6D\xB6",
+ ]
+ pattern = patterns[i - 5]
+
+ # Calculate repeat_count and the remainder
+ pattern_length = len(pattern)
+ repeat_count = file_size // pattern_length
+ remainder = file_size % pattern_length
+
+ # Create the final pattern
+ final_pattern = pattern * repeat_count + pattern[:remainder]
+ # Overwrite with the final pattern
+ DataManipulation.overwrite_with_pattern(file, final_pattern, file_size)
+ except Exception as e:
+ print()
+ logging.error(f"Error during Gutmann Wipe: {e}")
+
+ @staticmethod
+ def wallet_annihilation(filename, file, data, file_size):
+ """Overview:
+ This function first encrypts the wallet data, adding a layer of security, and then proceeds
+ to a comprehensive wiping process. It utilizes a combination of the DoD 5220.22-M, Schneier,
+ and Gutmann methods to ensure thorough overwriting and scrambling of the file data. The
+ encryption step is crucial as it secures the data before the wiping begins, making any
+ potential recovery attempts even more challenging. The sequence of wiping techniques aims
+ to achieve a high degree of certainty that the data cannot be recovered.
+
+ Parameters:
+ - filename: The name of the file to be wiped.
+ - file: File object representing the wallet file.
+ - data: The wallet data to be encrypted and then wiped.
+ - file_size: The total size of the file, guiding the scope of wiping.
+ """
+ try:
+ # Encryption with SHA3-512 hashes is 100% overkill but we need to ensure that the wallet data is unrecoverable
+ hmac_salt = hashlib.sha3_512().hexdigest()
+ verification_salt = hashlib.sha3_512().hexdigest()
+ password = hashlib.sha3_512().hexdigest()
+ verifier = verification_util.Verification.hash_password(password, verification_salt)
+ totp_secret = cryptographic_util.TOTP.generate_totp_secret(True, bytes(verification_salt,'utf-8'))
+ encrypted_data = cryptographic_util.EncryptDecryptUtils.encrypt_data(str(data), password, totp_secret, hmac_salt, verification_salt, verifier)
+ DataManipulation._save_data(filename, encrypted_data)
+ time.sleep(0.5)
+
+ random_bytes = bytearray(random.getrandbits(8) for _ in range(file_size))
+ DataManipulation.overwrite_with_pattern(file, random_bytes * file_size, file_size)
+ time.sleep(0.1)
+
+ DataManipulation.DoD_5220_22_M_wipe(file, file_size)
+ time.sleep(0.1)
+
+ DataManipulation.Schneier_wipe(file, file_size)
+ time.sleep(0.1)
+
+ DataManipulation.Gutmann_wipe(file, file_size)
+ except Exception as e:
+ print()
+ logging.error(f"Error during Wallet Annihilation: {e}")
+
+ @staticmethod
+ def delete_wallet(file_path, data, passes=1):
+ """Overview:
+ This is the main function for securely deleting wallet files. It locks the file to prevent
+ concurrent access, then executing the 'wallet_annihilation' method, which combines multiple
+ advanced wiping techniques, for each pass specified. The function ensures that the wallet
+ file is not just deleted, but it's data is irrecoverably destroyed in the event of too
+ many failed password attempts, or overwrite.
+
+ Parameters:
+ - file_path: The path to the wallet file.
+ - data: Data used in the annihilation process.
+ - passes: The number of annihilation cycles to execute.
+ """
+ if not os.path.exists(file_path):
+ raise ValueError("File does not exist")
+
+ lock = FileLock(file_path+".lock")
+ try:
+ with lock:
+ with open(file_path, "r+b") as file:
+ file_size = os.path.getsize(file.name)
+ if file_size == 0:
+ raise ValueError("File is empty")
+
+ for _ in range(passes):
+ DataManipulation.wallet_annihilation(file_path, file, data, file_size)
+ file.flush()
+ os.fsync(file.fileno())
+ with open(file_path, "w+b") as file:
+ file.truncate(0)
+ lock.release()
+ except IOError as e:
+ print()
+ logging.error(f"IOError during file overwrite: {e}")
+ except ValueError as e:
+ print()
+ logging.error(f"ValueError in file handling: {e}")
+ except Exception as e:
+ print()
+ logging.error(f"Unexpected error occurred: {e}")
+ finally:
+ lock.release()
+ try:
+ time.sleep(0.5)
+ os.remove(file_path)
+ if os.path.exists(file_path+".lock"):
+ os.remove(file_path+".lock")
+ sys.stdout.write("\rWallet Annihilation in progress....")
+ print()
+ except OSError as e:
+ print()
+ logging.error(f"Error while removing file: {e}")
\ No newline at end of file
diff --git a/denaro/wallet/interface_util.py b/denaro/wallet/utils/interface_util.py
similarity index 86%
rename from denaro/wallet/interface_util.py
rename to denaro/wallet/utils/interface_util.py
index cef6aa1..e36bbe9 100644
--- a/denaro/wallet/interface_util.py
+++ b/denaro/wallet/utils/interface_util.py
@@ -1,8 +1,4 @@
import qrcode
-from PIL import Image, ImageDraw
-from qrcode.image.styledpil import StyledPilImage
-from qrcode.image.styles.moduledrawers import CircleModuleDrawer
-from qrcode.image.styles.colormasks import SolidFillColorMask
import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame
@@ -16,7 +12,13 @@
import select
import sys
import base64
-from cryptographic_util import VerificationUtils, DataManipulation
+import data_manipulation_util
+import verification_util
+
+from PIL import Image, ImageDraw
+from qrcode.image.styledpil import StyledPilImage
+from qrcode.image.styles.moduledrawers import CircleModuleDrawer
+from qrcode.image.styles.colormasks import SolidFillColorMask
close_qr_window = False
@@ -86,7 +88,7 @@ def generate_qr_with_logo(data, logo_path):
qr_img.paste(logo_img, logo_pos, logo_img)
# Return the final QR code image with the logo
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not qr_img])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not qr_img])
return qr_img
@staticmethod
@@ -120,7 +122,7 @@ def generate_qr_gradient(image, palette):
draw.line([(x, 0), (x, height)], tuple(blended_color))
# Return the image with gradient applied
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not image])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not image])
return image
@staticmethod
@@ -185,7 +187,7 @@ def show_qr_with_timer(qr_image, filename, totp_secret):
# Capture window close event
if event.type == pygame.QUIT:
pygame.quit()
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return
# Capture window resize event
elif event.type == pygame.VIDEORESIZE:
@@ -248,7 +250,7 @@ def show_qr_with_timer(qr_image, filename, totp_secret):
pygame.display.flip()
# Quit pygame when the countdown reaches zero or the window is closed
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
pygame.quit()
@staticmethod
@@ -275,7 +277,7 @@ def wrap_text(text, font, max_width):
line = (line + ' ' + words.pop(0)).strip()
lines.append(line)
# Return the wrapped lines
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not lines])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not lines])
return lines
def close_qr_window(value):
@@ -330,7 +332,7 @@ def get_password(password=None):
password_confirm = password_input
# Check if the passwords match
if password_input == password_confirm:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not password_input])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not password_input])
return password_input
else:
print("Passwords do not match. Please try again.\n")
@@ -478,17 +480,17 @@ def backup_and_overwrite_helper(data, filename, password, encrypt, backup, disab
# Construct the backup filename
base_filename = os.path.basename(filename)
backup_name, _ = os.path.splitext(base_filename)
- backup_path = os.path.join("./wallets/wallet_backups", f"{backup_name}_backup_{datetime.datetime.fromtimestamp(int(time.time())).strftime('%Y-%m-%d_%H-%M-%S_%p')}") + ".json"
+ backup_path = os.path.join("./wallets/wallet_backups", f"{backup_name}_backup_{datetime.datetime.fromtimestamp(int(time.time())).strftime('%Y-%m-%d_%H-%M-%S')}") + ".json"
try:
# Create the backup
shutil.copy(filename, backup_path)
print(f"Backup created at {backup_path}")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return True
except Exception as e:
logging.error(f" Could not create backup: {e}\n")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return
else:
if not disable_warning:
@@ -512,15 +514,15 @@ def backup_and_overwrite_helper(data, filename, password, encrypt, backup, disab
if password and encrypt:
print("Overwrite password provided.")
# Verify the password and HMAC to prevent brute force
- password_verified, hmac_verified, _ = VerificationUtils.verify_password_and_hmac(data, password, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
+ password_verified, hmac_verified, _ = verification_util.Verification.verify_password_and_hmac(data, password, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
# Based on password verification, update or reset the number of failed attempts
- data = DataManipulation.update_or_reset_attempts(data, filename, base64.b64decode(data["wallet_data"]["hmac_salt"]), password_verified, deterministic)
- DataManipulation._save_data(filename,data)
+ data = data_manipulation_util.DataManipulation.update_or_reset_attempts(data, filename, base64.b64decode(data["wallet_data"]["hmac_salt"]), password_verified, deterministic)
+ data_manipulation_util.DataManipulation._save_data(filename,data)
# Check if there is still wallet data verify the password and HMAC again
if data:
- password_verified, hmac_verified, _ = VerificationUtils.verify_password_and_hmac(data, password, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
+ password_verified, hmac_verified, _ = verification_util.Verification.verify_password_and_hmac(data, password, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
# Handle error if the password and HMAC verification failed
if not (password_verified and hmac_verified):
logging.error("Authentication failed or wallet data is corrupted.")
@@ -532,15 +534,15 @@ def backup_and_overwrite_helper(data, filename, password, encrypt, backup, disab
# Prompt user for password
password_input = UserPrompts.get_password(password=password if password and (password_verified and hmac_verified) else None)
# Verify the password and HMAC
- password_verified, hmac_verified, _ = VerificationUtils.verify_password_and_hmac(data, password_input, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
+ password_verified, hmac_verified, _ = verification_util.Verification.verify_password_and_hmac(data, password_input, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
# Based on password verification, update or reset the number of failed attempts
- data = DataManipulation.update_or_reset_attempts(data, filename, base64.b64decode(data["wallet_data"]["hmac_salt"]), password_verified, deterministic)
- DataManipulation._save_data(filename,data)
+ data = data_manipulation_util.DataManipulation.update_or_reset_attempts(data, filename, base64.b64decode(data["wallet_data"]["hmac_salt"]), password_verified, deterministic)
+ data_manipulation_util.DataManipulation._save_data(filename,data)
# If wallet data has not erased yet verify the password and HMAC again
if data:
- password_verified, hmac_verified, _ = VerificationUtils.verify_password_and_hmac(data, password_input, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
+ password_verified, hmac_verified, _ = verification_util.Verification.verify_password_and_hmac(data, password_input, base64.b64decode(data["wallet_data"]["hmac_salt"]), base64.b64decode(data["wallet_data"]["verification_salt"]), deterministic)
# Handle error if the password and HMAC verification failed
if data and not (password_verified and hmac_verified):
@@ -559,26 +561,26 @@ def backup_and_overwrite_helper(data, filename, password, encrypt, backup, disab
print()
# Call wait_for_input and allow up to 5 seconds for the user to cancel overwrite operation
if not UserPrompts.wait_for_input(timeout=5):
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return
# If no input is recieved within 5 seconds then continue
else:
print()
try:
# Overwrite wallet with empty data
- DataManipulation.delete_wallet(filename, data)
+ data_manipulation_util.DataManipulation.delete_wallet(filename, data)
print("Wallet data has been erased.")
time.sleep(0.5)
except Exception as e:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return True
else:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return True
else:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return
@staticmethod
@@ -600,11 +602,11 @@ def handle_2fa_validation(data, totp_code=None):
# Check if a TOTP code was already provided
if not totp_code:
# Get TOTP code from user input
- totp_code = input("Please enter the Two-Factor Authentication code from your autthenticator app (or type '/q' to exit the script): ")
+ totp_code = input("Please enter the Two-Factor Authentication code from your authenticator app (or type '/q' to exit the script): ")
# Exit if the user chooses to quit
if totp_code.lower() == '/q':
logging.info("User exited before providing a valid Two-Factor Authentication code.\n")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return False
# Check if the totp_code is provided
if not totp_code:
@@ -622,12 +624,12 @@ def handle_2fa_validation(data, totp_code=None):
totp_code = None
continue
# Validate the TOTP code using utility method
- if VerificationUtils.validate_totp_code(data, totp_code):
+ if verification_util.Verification.validate_totp_code(data, totp_code):
result = {"valid": True, "totp_secret": data}
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
else:
logging.error("Authentication failed. Please try again.\n")
# Reset TOTP code and continue the loop
totp_code = None
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
\ No newline at end of file
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
\ No newline at end of file
diff --git a/denaro/wallet/utils/transaction_utils/coinbase_transaction.py b/denaro/wallet/utils/transaction_utils/coinbase_transaction.py
new file mode 100644
index 0000000..f2667e1
--- /dev/null
+++ b/denaro/wallet/utils/transaction_utils/coinbase_transaction.py
@@ -0,0 +1,50 @@
+import os
+import sys
+from decimal import Decimal
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+sys.path.insert(0, dir_path + "/../")
+from wallet_generation_util import sha256, ENDIAN
+
+from .transaction_output import TransactionOutput
+
+class CoinbaseTransaction:
+ _hex: str = None
+
+ def __init__(self, block_hash: str, address: str, amount: Decimal):
+ self.block_hash = block_hash
+ self.address = address
+ self.amount = amount
+ self.outputs = [TransactionOutput(address, amount)]
+
+ #async def verify(self):
+ # from .. import Database
+ # block = await (await Database.get()).get_block(self.block_hash)
+ # return block['address'] == self.address and self.amount == block['reward']
+
+ def hex(self):
+ if self._hex is not None:
+ return self._hex
+ hex_inputs = (bytes.fromhex(self.block_hash) + (0).to_bytes(1, ENDIAN)).hex()
+ hex_outputs = ''.join(tx_output.tobytes().hex() for tx_output in self.outputs)
+
+ if all(len(tx_output.address_bytes) == 64 for tx_output in self.outputs):
+ version = 1
+ elif all(len(tx_output.address_bytes) == 33 for tx_output in self.outputs):
+ version = 2
+ else:
+ raise NotImplementedError()
+
+ self._hex = ''.join([
+ version.to_bytes(1, ENDIAN).hex(),
+ (1).to_bytes(1, ENDIAN).hex(),
+ hex_inputs,
+ (1).to_bytes(1, ENDIAN).hex(),
+ hex_outputs,
+ (36).to_bytes(1, ENDIAN).hex(),
+ ])
+
+ return self._hex
+
+ def hash(self):
+ return sha256(self.hex())
\ No newline at end of file
diff --git a/denaro/wallet/utils/transaction_utils/transaction.py b/denaro/wallet/utils/transaction_utils/transaction.py
new file mode 100644
index 0000000..7ef11ae
--- /dev/null
+++ b/denaro/wallet/utils/transaction_utils/transaction.py
@@ -0,0 +1,268 @@
+import os
+import sys
+from decimal import Decimal
+from io import BytesIO
+from typing import List
+
+from fastecdsa import keys
+from icecream import ic
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+sys.path.insert(0, dir_path + "/../")
+from wallet_generation_util import point_to_string, bytes_to_string, sha256, ENDIAN, CURVE, SMALLEST
+
+from .transaction_input import TransactionInput
+from .transaction_output import TransactionOutput
+from .coinbase_transaction import CoinbaseTransaction
+
+#print = ic
+
+
+class Transaction:
+ def __init__(self, inputs: List[TransactionInput], outputs: List[TransactionOutput], message: bytes = None, version: int = None):
+ if len(inputs) >= 256:
+ raise Exception(f'You can spend max 255 inputs in a single transactions, not {len(inputs)}')
+ if len(outputs) >= 256:
+ raise Exception(f'You can have max 255 outputs in a single transactions, not {len(outputs)}')
+ self.inputs = inputs
+ self.outputs = outputs
+ self.message = message
+ if version is None:
+ if all(len(tx_output.address_bytes) == 64 for tx_output in outputs):
+ version = 1
+ elif all(len(tx_output.address_bytes) == 33 for tx_output in outputs):
+ version = 3
+ else:
+ raise NotImplementedError()
+ if version > 3:
+ raise NotImplementedError()
+ self.version = version
+ self._hex: str = None
+ self.fees: Decimal = None
+ self.tx_hash: str = None
+
+ def hex(self, full: bool = True):
+ inputs, outputs = self.inputs, self.outputs
+ hex_inputs = ''.join(tx_input.tobytes().hex() for tx_input in inputs)
+ hex_outputs = ''.join(tx_output.tobytes().hex() for tx_output in outputs)
+
+ version = self.version
+
+ self._hex = ''.join([
+ version.to_bytes(1, ENDIAN).hex(),
+ len(inputs).to_bytes(1, ENDIAN).hex(),
+ hex_inputs,
+ (len(outputs)).to_bytes(1, ENDIAN).hex(),
+ hex_outputs
+ ])
+
+ if not full and (version <= 2 or self.message is None):
+ return self._hex
+
+ if self.message is not None:
+ if version <= 2:
+ self._hex += bytes([1, len(self.message)]).hex()
+ else:
+ self._hex += bytes([1]).hex()
+ self._hex += (len(self.message)).to_bytes(2, ENDIAN).hex()
+
+ self._hex += self.message.hex()
+ if not full:
+ return self._hex
+ else:
+ self._hex += (0).to_bytes(1, ENDIAN).hex()
+
+ signatures = []
+ for tx_input in inputs:
+ signed = tx_input.get_signature()
+ if signed not in signatures:
+ signatures.append(signed)
+ self._hex += signed
+
+ return self._hex
+
+ def hash(self):
+ if self.tx_hash is None:
+ self.tx_hash = sha256(self.hex())
+ return self.tx_hash
+
+ def _verify_double_spend_same_transaction(self):
+ used_inputs = []
+ for tx_input in self.inputs:
+ input_hash = f"{tx_input.tx_hash}{tx_input.index}"
+ if input_hash in used_inputs:
+ return False
+ used_inputs.append(input_hash)
+ return True
+
+ #async def verify_double_spend(self):
+ # from .. import Database
+ # check_inputs = [(tx_input.tx_hash, tx_input.index) for tx_input in self.inputs]
+ # unspent_outputs = await Database.instance.get_unspent_outputs(check_inputs)
+ # return set(check_inputs) == set(unspent_outputs)
+
+ #async def verify_double_spend_pending(self):
+ # from .. import Database
+ # check_inputs = [(tx_input.tx_hash, tx_input.index) for tx_input in self.inputs]
+ # spent_outputs = await Database.instance.get_pending_spent_outputs(check_inputs)
+ # return spent_outputs == []
+
+ async def _fill_transaction_inputs(self, txs=None) -> None:
+ #from .. import Database
+ check_inputs = [tx_input.tx_hash for tx_input in self.inputs if tx_input.transaction is None and tx_input.transaction_info is None]
+ if not check_inputs:
+ return
+ #if txs is None:
+ # txs = await Database.instance.get_transactions_info(check_inputs)
+ for tx_input in self.inputs:
+ tx_hash = tx_input.tx_hash
+ if tx_hash in txs:
+ tx_input.transaction_info = txs[tx_hash]
+
+ async def _check_signature(self):
+ tx_hex = self.hex(False)
+ checked_signatures = []
+ for tx_input in self.inputs:
+ if tx_input.signed is None:
+ print('not signed')
+ return False
+ await tx_input.get_public_key()
+ signature = (tx_input.public_key, tx_input.signed)
+ if signature in checked_signatures:
+ continue
+ if not await tx_input.verify(tx_hex):
+ print('signature not valid')
+ return False
+ checked_signatures.append(signature)
+ return True
+
+ def _verify_outputs(self):
+ return (self.outputs or self.hash() == '915ddf143e14647ba1e04c44cf61e57084254c44cd4454318240f359a414065c') and all(tx_output.verify() for tx_output in self.outputs)
+
+ async def verify(self, check_double_spend: bool = True) -> bool:
+ if check_double_spend and not self._verify_double_spend_same_transaction():
+ print('double spend inside same transaction')
+ return False
+
+ #if check_double_spend and not await self.verify_double_spend():
+ # print('double spend')
+ # return False
+
+ await self._fill_transaction_inputs()
+
+ if not await self._check_signature():
+ return False
+
+ if not self._verify_outputs():
+ print('invalid outputs')
+ return False
+
+ if await self.get_fees() < 0:
+ print('We are not the Federal Reserve')
+ return False
+
+ return True
+
+ #async def verify_pending(self):
+ # return await self.verify() and await self.verify_double_spend_pending()
+
+ def sign(self, private_keys: list = []):
+ for private_key in private_keys:
+ for input in self.inputs:
+ if input.private_key is None and (input.public_key or input.transaction):
+ public_key = keys.get_public_key(private_key, CURVE)
+ input_public_key = input.public_key or input.transaction.outputs[input.index].public_key
+ if public_key == input_public_key:
+ input.private_key = private_key
+ for input in self.inputs:
+ if input.private_key is not None:
+ input.sign(self.hex(False))
+ return self
+
+ async def get_fees(self):
+ input_amount = 0
+ for tx_input in self.inputs:
+ input_amount += await tx_input.get_amount()
+
+ output_amount = sum(tx_output.amount for tx_output in self.outputs)
+
+ self.fees = input_amount - output_amount
+ assert (self.fees * SMALLEST) % 1 == 0.0
+ return self.fees
+
+ @staticmethod
+ async def from_hex(hexstring: str, check_signatures: bool = True):
+ tx_bytes = BytesIO(bytes.fromhex(hexstring))
+ version = int.from_bytes(tx_bytes.read(1), ENDIAN)
+ if version > 3:
+ raise NotImplementedError()
+
+ inputs_count = int.from_bytes(tx_bytes.read(1), ENDIAN)
+
+ inputs = []
+
+ for i in range(0, inputs_count):
+ tx_hex = tx_bytes.read(32).hex()
+ tx_index = int.from_bytes(tx_bytes.read(1), ENDIAN)
+ inputs.append(TransactionInput(tx_hex, index=tx_index))
+
+ outputs_count = int.from_bytes(tx_bytes.read(1), ENDIAN)
+
+ outputs = []
+
+ for i in range(0, outputs_count):
+ pubkey = tx_bytes.read(64 if version == 1 else 33)
+ amount_length = int.from_bytes(tx_bytes.read(1), ENDIAN)
+ amount = int.from_bytes(tx_bytes.read(amount_length), ENDIAN) / Decimal(SMALLEST)
+ outputs.append(TransactionOutput(bytes_to_string(pubkey), amount))
+
+ specifier = int.from_bytes(tx_bytes.read(1), ENDIAN)
+ if specifier == 36:
+ assert len(inputs) == 1 and len(outputs) == 1
+ return CoinbaseTransaction(inputs[0].tx_hash, outputs[0].address, outputs[0].amount)
+ else:
+ if specifier == 1:
+ message_length = int.from_bytes(tx_bytes.read(1 if version <= 2 else 2), ENDIAN)
+ message = tx_bytes.read(message_length)
+ else:
+ message = None
+ assert specifier == 0
+
+ signatures = []
+
+ while True:
+ signed = (int.from_bytes(tx_bytes.read(32), ENDIAN), int.from_bytes(tx_bytes.read(32), ENDIAN))
+ if signed[0] == 0:
+ break
+ signatures.append(signed)
+
+ if len(signatures) == 1:
+ for tx_input in inputs:
+ tx_input.signed = signatures[0]
+ elif len(inputs) == len(signatures):
+ for i, tx_input in enumerate(inputs):
+ tx_input.signed = signatures[i]
+ else:
+ if not check_signatures:
+ return Transaction(inputs, outputs, message, version)
+ index = {}
+ for tx_input in inputs:
+ public_key = point_to_string(await tx_input.get_public_key())
+ if public_key not in index.keys():
+ index[public_key] = []
+ index[public_key].append(tx_input)
+
+ for i, signed in enumerate(signatures):
+ for tx_input in index[list(index.keys())[i]]:
+ tx_input.signed = signed
+
+ return Transaction(inputs, outputs, message, version)
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.hex() == other.hex()
+ else:
+ return False
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
\ No newline at end of file
diff --git a/denaro/wallet/utils/transaction_utils/transaction_input.py b/denaro/wallet/utils/transaction_utils/transaction_input.py
new file mode 100644
index 0000000..77c66bf
--- /dev/null
+++ b/denaro/wallet/utils/transaction_utils/transaction_input.py
@@ -0,0 +1,98 @@
+import os
+import sys
+from decimal import Decimal
+from typing import Tuple
+
+from fastecdsa import ecdsa
+from fastecdsa.point import Point
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+sys.path.insert(0, dir_path + "/../")
+from wallet_generation_util import string_to_point, point_to_string, ENDIAN, CURVE, SMALLEST
+
+
+class TransactionInput:
+ public_key = None
+
+ signed: Tuple[int, int] = None
+ amount: Decimal = None
+
+ def __init__(self, input_tx_hash: str, index: int, private_key: int = None, transaction=None, amount: Decimal = None, public_key: Point = None):
+ self.tx_hash = input_tx_hash
+ self.index = index
+ self.private_key = private_key
+ self.transaction = transaction
+ self.transaction_info = None
+ self.amount = amount
+ self.public_key = public_key
+ if transaction is not None and amount is None:
+ self.get_related_output()
+
+ async def get_transaction(self):
+ return self.transaction
+
+ async def get_transaction_info(self):
+ assert self.transaction_info is not None
+ return self.transaction_info
+
+ async def get_related_output(self):
+ tx = await self.get_transaction()
+ related_output = tx.outputs[self.index]
+ self.amount = related_output.amount
+ return related_output
+
+ async def get_related_output_info(self):
+ tx = await self.get_transaction_info()
+ related_output = {'address': tx['outputs_addresses'][self.index], 'amount': Decimal(tx['outputs_amounts'][self.index]) / SMALLEST}
+ self.amount = related_output['amount']
+ return related_output
+
+ async def get_amount(self):
+ if self.amount is None:
+ if self.transaction is not None:
+ return self.transaction.outputs[self.index].amount
+ else:
+ await self.get_related_output_info()
+ return self.amount
+
+ async def get_address(self):
+ if self.transaction is not None:
+ return (await self.get_related_output()).address
+ return (await self.get_related_output_info())['address']
+
+ def sign(self, tx_hex: str, private_key: int = None):
+ private_key = private_key if private_key is not None else self.private_key
+ self.signed = ecdsa.sign(bytes.fromhex(tx_hex), private_key)
+
+ async def get_public_key(self):
+ return self.public_key or string_to_point(await self.get_address())
+
+ def tobytes(self):
+ return bytes.fromhex(self.tx_hash) + self.index.to_bytes(1, ENDIAN)
+
+ def get_signature(self):
+ return self.signed[0].to_bytes(32, ENDIAN).hex() + self.signed[1].to_bytes(32, ENDIAN).hex()
+
+ async def verify(self, input_tx) -> bool:
+ try:
+ public_key = await self.get_public_key()
+ except AssertionError:
+ return False
+ # print('verifying with', point_to_string(public_key))
+
+ return \
+ ecdsa.verify(self.signed, bytes.fromhex(input_tx), public_key, CURVE) or \
+ ecdsa.verify(self.signed, input_tx, public_key, CURVE)
+
+ @property
+ def as_dict(self):
+ self_dict = vars(self).copy()
+ self_dict['signed'] = self_dict['signed'] is not None
+ if 'public_key' in self_dict: self_dict['public_key'] = point_to_string(self_dict['public_key'])
+ if 'transaction' in self_dict: del self_dict['transaction']
+ if 'private_key' in self_dict: del self_dict['private_key']
+ return self_dict
+
+ def __eq__(self, other):
+ assert isinstance(other, self.__class__)
+ return (self.tx_hash, self.index) == (other.tx_hash, other.index)
\ No newline at end of file
diff --git a/denaro/wallet/utils/transaction_utils/transaction_output.py b/denaro/wallet/utils/transaction_utils/transaction_output.py
new file mode 100644
index 0000000..b6d3f2c
--- /dev/null
+++ b/denaro/wallet/utils/transaction_utils/transaction_output.py
@@ -0,0 +1,33 @@
+import os
+import sys
+from decimal import Decimal
+
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+sys.path.insert(0, dir_path + "/../")
+from wallet_generation_util import byte_length, string_to_point, string_to_bytes, ENDIAN, CURVE, SMALLEST
+
+class TransactionOutput:
+ def __init__(self, address: str, amount: Decimal):
+ from fastecdsa.point import Point
+ if isinstance(address, Point):
+ raise Exception('TransactionOutput does not accept Point anymore. Pass the address string instead')
+ self.address = address
+ self.address_bytes = string_to_bytes(address)
+ self.public_key = string_to_point(address)
+ assert (amount * SMALLEST) % 1 == 0.0, 'too many decimal digits'
+ self.amount = amount
+
+ def tobytes(self):
+ amount = int(self.amount * SMALLEST)
+ count = byte_length(amount)
+ return self.address_bytes + count.to_bytes(1, ENDIAN) + amount.to_bytes(count, ENDIAN)
+
+ def verify(self):
+ return self.amount > 0 and CURVE.is_point_on_curve((self.public_key.x, self.public_key.y))
+
+ @property
+ def as_dict(self):
+ res = vars(self).copy()
+ if 'public_key' in res: del res['public_key']
+ return res
\ No newline at end of file
diff --git a/denaro/wallet/utils/verification_util.py b/denaro/wallet/utils/verification_util.py
new file mode 100644
index 0000000..e0889d5
--- /dev/null
+++ b/denaro/wallet/utils/verification_util.py
@@ -0,0 +1,255 @@
+import hashlib
+import hmac as hmac_module
+import json
+import base64
+import pyotp
+import logging
+import requests
+import urllib3
+urllib3.disable_warnings()
+import re
+import random
+
+from Crypto.Protocol.KDF import scrypt
+import data_manipulation_util
+import cryptographic_util
+
+class Verification:
+ """
+ Handles data verification.
+ """
+ @staticmethod
+ def hash_password(password, salt):
+ """
+ Generate a cryptographic hash of the password using PBKDF2 and then Scrypt.
+ """
+ # First layer of hashing using PBKDF2
+ salt_bytes = salt
+ if not isinstance(salt, bytes):
+ salt_bytes = bytes(salt, 'utf-8')
+ pbkdf2_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt_bytes, 100000)
+
+ # Second layer of hashing using Scrypt
+ result = scrypt(pbkdf2_hash, salt=salt, key_len=32, N=2**14, r=8, p=1)
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def verify_password(stored_password_hash, provided_password, salt):
+ """
+ Compares the provided password with the stored hash.
+ """
+ # Generate hash of the provided password
+ verifier = Verification.hash_password(provided_password, salt)
+ # Securely compare the generated hash with the stored hash
+ is_verified = hmac_module.compare_digest(verifier, stored_password_hash)
+
+ # Nullify verifier if not verified
+ if not is_verified:
+ verifier = None
+ result = is_verified, verifier
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def hmac_util(password=None,hmac_salt=None,stored_hmac=None, hmac_msg=None, verify=False):
+ """
+ Handle HMAC generation and verification.
+ """
+ # Generate HMAC key using Scrypt
+ hmac_key = scrypt(password.encode(), salt=hmac_salt, key_len=32, N=2**14, r=8, p=1)
+ # Generate HMAC of the message
+ computed_hmac = hmac_module.new(hmac_key, hmac_msg, hashlib.sha256).digest()
+ # If in verify mode, securely compare the computed HMAC with the stored HMAC
+ if verify:
+ result = hmac_module.compare_digest(computed_hmac, stored_hmac)
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+ else:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not computed_hmac])
+ return computed_hmac
+
+ @staticmethod
+ def verify_password_and_hmac(data, password, hmac_salt, verification_salt, deterministic):
+ """
+ Verifies the given password and HMAC.
+
+ Arguments:
+ - data: The wallet data
+ - password: The user's password
+ - hmac_salt: The HMAC salt
+
+ Returns:
+ - A tuple of booleans indicating if the password and HMAC are verified
+ """
+ # Decode and verify the stored password verifier
+ stored_verifier = base64.b64decode(data["wallet_data"]["verifier"].encode('utf-8'))
+ password_verified, _ = Verification.verify_password(stored_verifier, password, verification_salt)
+
+ # Prepare and verify the HMAC message
+ hmac_msg = json.dumps(data["wallet_data"]["entry_data"]["entries"]).encode()
+ if "imported_entries" in data["wallet_data"]["entry_data"]:
+ hmac_msg = json.dumps(data["wallet_data"]["entry_data"]["imported_entries"]).encode() + hmac_msg
+ if deterministic:
+ hmac_msg += json.dumps(data["wallet_data"]["entry_data"]["key_data"]).encode()
+ stored_hmac = base64.b64decode(data["wallet_data"]["hmac"].encode('utf-8'))
+ hmac_verified = Verification.hmac_util(password=password, hmac_salt=hmac_salt, stored_hmac=stored_hmac, hmac_msg=hmac_msg, verify=True)
+ result = password_verified, hmac_verified, stored_verifier
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def verify_totp_secret(password,totp_secret,hmac_salt,verification_salt,stored_verifier):
+ """
+ Validates the given Two-Factor Authentication secret token
+ """
+ # Decrypt the stored TOTP secret to handle 2FA
+ decrypted_totp_secret = cryptographic_util.EncryptDecryptUtils.decrypt_data(totp_secret, password, "", hmac_salt, verification_salt, stored_verifier)
+ # Generate a predictable TOTP secret to check against
+ predictable_totp_secret = cryptographic_util.TOTP.generate_totp_secret(True,verification_salt)
+ # If the decrypted TOTP doesn't match the predictable one, handle 2FA validation
+ if decrypted_totp_secret != predictable_totp_secret:
+ result = decrypted_totp_secret, True
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+ else:
+ result = "", False
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def validate_totp_code(secret, code):
+ """
+ Validates the given Two-Factor Authentication code using the provided secret.
+ """
+ totp = pyotp.TOTP(secret)
+ result = totp.verify(code)
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+ @staticmethod
+ def is_valid_address(address):
+ ipv4_with_optional_port = r'^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::([0-5]?[0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$'
+ url_with_tld_optional_port = r'^(?!(http:\/\/|https:\/\/))[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}(?::([0-5]?[0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$'
+ localhost_with_optional_port = r'^localhost(:([1-9]\d{0,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]))?$'
+
+ stripped_address = re.sub(r'^https?://', '', address)
+ domain_pattern = url_with_tld_optional_port
+ if re.match(domain_pattern, stripped_address, re.IGNORECASE):
+ return True
+ if re.match(ipv4_with_optional_port, stripped_address):
+ return True
+ if re.match(localhost_with_optional_port,stripped_address):
+ return True
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return False
+
+ @staticmethod
+ def is_valid_port(port):
+ try:
+ port_num = int(port)
+ return 1 <= port_num <= 65535
+ except ValueError:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return False
+
+ @staticmethod
+ def validate_node_address(node):
+ node = node.rstrip('//')
+ chosen_protocol = 2
+ if node.startswith("https://"):
+ chosen_protocol = 0
+ elif node.startswith("http://"):
+ chosen_protocol = 1
+
+ if not Verification.is_valid_address(node):
+ logging.error("Invalid node address. Please provide a valid IP Address or URL.")
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return False, None
+
+ node = re.sub(r'\s+', ' ', node)
+ node = node.replace(" :", ":").replace(": ", ":")
+
+ #This may be redundant, but for good measure we'll perform additional port number validations
+ port_match = re.search(r':([0-9]{1,5})', node)
+ if port_match:
+ if not Verification.is_valid_port(port_match.group(1)):
+ logging.error("Invalid port number. Please enter a value between 1 and 65535.")
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return False, None
+
+ stripped_address = re.sub(r'^https?://', '', node)
+ success, node = Verification.try_request(stripped_address, chosen_protocol)
+ if success:
+ print(f"Successfully established connection with valid Denaro node at: {node}\n")
+ return True, node
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return False, None
+
+ @staticmethod
+ def try_request(address, chosen_protocol):
+ main_node_url = "https://denaro-node.gaetano.eu.org"
+ protocols = ["https://", "http://"]
+
+ # Configure logging for detailed error information
+ logging.basicConfig(level=logging.DEBUG)
+
+ # Check if the address already includes a protocol
+ if re.match(r'^https?://', address):
+ protocols_to_try = [""]
+ else:
+ protocols_to_try = protocols if chosen_protocol == 2 else [protocols[chosen_protocol], protocols[1 - chosen_protocol]]
+
+ for index, protocol in enumerate(protocols_to_try):
+ full_address = protocol + address
+
+ try:
+ # Get the last block number from the main node
+ main_response = requests.get(f"{main_node_url}/get_mining_info", timeout=5, verify=False)
+ main_response.raise_for_status()
+ last_block_info = main_response.json().get('result', {}).get('last_block', {})
+ last_block_number = last_block_info.get('id')
+ if last_block_number is None:
+ logging.warning("Last block number not found in main node response.")
+ continue
+
+ # Generate a random block number
+ random_block_id = random.randint(0, last_block_number - 1)
+
+ # Get the block hash from the main node
+ main_block_response = requests.get(f"{main_node_url}/get_block?block={random_block_id}", timeout=5, verify=False)
+ main_block_response.raise_for_status()
+ main_node_block_info = main_block_response.json().get('result', {}).get('block', {})
+ main_node_block_hash = main_node_block_info.get('hash')
+ if main_node_block_hash is None:
+ logging.warning("Block hash not found in main node response.")
+ continue
+
+ # Get the block hash from the user-specified node
+ user_block_response = requests.get(f"{full_address}/get_block?block={random_block_id}", timeout=5, verify=False)
+ user_block_response.raise_for_status()
+ user_node_block_info = user_block_response.json().get('result', {}).get('block', {})
+ user_node_block_hash = user_node_block_info.get('hash')
+
+ if user_node_block_hash is None:
+ logging.warning("Block hash not found in user-specified node response.")
+ continue
+
+ # Compare the block hashes
+ if main_node_block_hash == user_node_block_hash:
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return True, full_address
+ else:
+ logging.info(f"Node at {full_address} has invalid blockchain data.")
+ continue
+
+ except requests.RequestException:
+ if index < len(protocols_to_try) - 1:
+ logging.warning(f"Connection failed with {full_address}. Trying next protocol...")
+ else:
+ logging.warning(f"Connection failed with {full_address}.\nUsing default Denaro node at: {main_node_url}\n")
+ continue
+
+ # Secure delete before final return
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, False
\ No newline at end of file
diff --git a/denaro/key_generation.py b/denaro/wallet/utils/wallet_generation_util.py
similarity index 77%
rename from denaro/key_generation.py
rename to denaro/wallet/utils/wallet_generation_util.py
index 5190f8f..2618680 100644
--- a/denaro/key_generation.py
+++ b/denaro/wallet/utils/wallet_generation_util.py
@@ -19,11 +19,7 @@
from icecream import ic
import binascii
-# Get the absolute path of the directory containing the current script.
-dir_path = os.path.dirname(os.path.realpath(__file__))
-# Insert folder paths for modules
-sys.path.insert(0, dir_path + "/wallet")
-from denaro.wallet.cryptographic_util import DataManipulation
+import data_manipulation_util
# Custom print function definition
_print = print # Saving the original print function for later use
@@ -31,6 +27,7 @@
# Constants
ENDIAN = 'little' # Defining byte order as little-endian
CURVE = curve.P256 # Defining the elliptic curve for ECDSA
+SMALLEST = 1000000
# Logging Configuration
# Set logging level based on command-line arguments
@@ -45,7 +42,7 @@ def log(s):
"""
# Logging the message under the 'denaro' namespace
logging.getLogger('denaro').info(s)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
# Configure Icecream for custom logging
ic.configureOutput(outputFunction=log) # Redirecting icecream output to custom log function
@@ -62,7 +59,7 @@ def get_json(obj):
"""
# Convert object to JSON and then deserialize it to dictionary
result = json.loads(json.dumps(obj, default=lambda o: getattr(o, 'as_dict', getattr(o, '__dict__', str(o)))))
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def timestamp():
@@ -74,7 +71,7 @@ def timestamp():
"""
# Getting current time, setting it to UTC and returning its timestamp
result = int(datetime.now(timezone.utc).replace(tzinfo=timezone.utc).timestamp())
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def sha256(message: Union[str, bytes]):
@@ -92,7 +89,7 @@ def sha256(message: Union[str, bytes]):
message = bytes.fromhex(message)
# Calculate SHA-256 hash and return it as a hexadecimal string
result = hashlib.sha256(message).hexdigest()
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def byte_length(i: int):
@@ -107,7 +104,7 @@ def byte_length(i: int):
"""
# Calculate byte length using bit length and ceiling function
result = ceil(i.bit_length() / 8.0)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def normalize_block(block) -> dict:
@@ -126,7 +123,7 @@ def normalize_block(block) -> dict:
block['address'] = block['address'].strip(' ')
# Convert and normalize the 'timestamp' field to UTC timestamp
block['timestamp'] = int(block['timestamp'].replace(tzinfo=timezone.utc).timestamp())
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not block])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not block])
return block
def x_to_y(x: int, is_odd: bool = False):
@@ -148,7 +145,7 @@ def x_to_y(x: int, is_odd: bool = False):
y_res, y_mod = mod_sqrt(y2, p)
# Return either y_res or y_mod based on whether y should be odd
result = y_res if y_res % 2 == is_odd else y_mod
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
class AddressFormat(Enum):
@@ -172,17 +169,17 @@ def bytes_to_point(point_bytes: bytes) -> Point:
if len(point_bytes) == 64:
x, y = int.from_bytes(point_bytes[:32], ENDIAN), int.from_bytes(point_bytes[32:], ENDIAN) # Extract x and y from bytes
result = Point(x, y, CURVE) # Return as Point object
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
# If the byte length is 33, it's a compressed point
elif len(point_bytes) == 33:
specifier = point_bytes[0] # First byte is the specifier for odd/even y-coordinate
x = int.from_bytes(point_bytes[1:], ENDIAN) # Extract x from the bytes
result = Point(x, x_to_y(x, specifier == 43)) # Compute y and return as Point object
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
else:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
# Unsupported byte length
raise NotImplementedError()
@@ -203,11 +200,11 @@ def bytes_to_string(point_bytes: bytes) -> str:
elif len(point_bytes) == 33:
address_format = AddressFormat.COMPRESSED # Compressed format
else:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
# Unsupported byte length
raise NotImplementedError()
result = point_to_string(point, address_format) # Convert point to string based on the determined format
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def point_to_bytes(point: Point, address_format: AddressFormat = AddressFormat.FULL_HEX) -> bytes:
@@ -224,15 +221,15 @@ def point_to_bytes(point: Point, address_format: AddressFormat = AddressFormat.F
# If full hexadecimal format is chosen
if address_format is AddressFormat.FULL_HEX:
result = point.x.to_bytes(32, byteorder=ENDIAN) + point.y.to_bytes(32, byteorder=ENDIAN)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
# If compressed format is chosen
elif address_format is AddressFormat.COMPRESSED:
result = string_to_bytes(point_to_string(point, AddressFormat.COMPRESSED))
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
else:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
# Raise an exception for unsupported formats
raise NotImplementedError()
@@ -252,17 +249,17 @@ def point_to_string(point: Point, address_format: AddressFormat = AddressFormat.
if address_format is AddressFormat.FULL_HEX:
point_bytes = point_to_bytes(point) # Convert point to bytes
result = point_bytes.hex() # Convert bytes to hexadecimal string
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
# For compressed format
elif address_format is AddressFormat.COMPRESSED:
# Convert point to Base58 string
address = base58.b58encode((42 if y % 2 == 0 else 43).to_bytes(1, ENDIAN) + x.to_bytes(32, ENDIAN))
result = address if isinstance(address, str) else address.decode('utf-8') # Ensure the result is a string
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
else:
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
# Unsupported format
raise NotImplementedError()
@@ -279,11 +276,11 @@ def string_to_bytes(string: str) -> bytes:
try:
# Try to convert from hexadecimal to bytes
point_bytes = bytes.fromhex(string)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not point_bytes])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not point_bytes])
except ValueError:
# If not hexadecimal, assume it's Base58 and decode it
point_bytes = base58.b58decode(string)
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not point_bytes])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not point_bytes])
return point_bytes
def string_to_point(string: str):
@@ -298,7 +295,7 @@ def string_to_point(string: str):
"""
# Convert the string to bytes and then to an ECDSA point
result = bytes_to_point(string_to_bytes(string))
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def hex_to_point(x_hex: str, y_hex: str, curve_obj):
@@ -316,7 +313,7 @@ def hex_to_point(x_hex: str, y_hex: str, curve_obj):
x_int = int(x_hex, 16) # Convert x from hex to integer
y_int = int(y_hex, 16) # Convert y from hex to integer
result = Point(x_int, y_int, curve_obj) # Create and return the ECDSA point
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def private_to_public_key_fastecdsa(private_key_hex):
@@ -343,7 +340,7 @@ def private_to_public_key_fastecdsa(private_key_hex):
# Return the public point and its compressed representation
result = public_point, compressed_public_key
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
def generate(mnemonic_phrase=None, passphrase=None, index=0, deterministic=False, fields=None):
@@ -413,5 +410,26 @@ def generate(mnemonic_phrase=None, passphrase=None, index=0, deterministic=False
result["public_key"] = public_key_hex
if "address" in fields:
result["address"] = address
- DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result # Return the generated information as a dictionary
+
+def generate_from_private_key(private_key_hex, fields=None):
+
+ public_key_point, public_key_hex = private_to_public_key_fastecdsa(private_key_hex) # Get public key
+ address = point_to_string(public_key_point) # Get address
+
+ # Define default fields if not specified
+ if fields is None:
+ fields = ["mnemonic", "private_key", "public_key", "address"]
+
+ result = {} # Dictionary to store the result
+ # Populate result based on specified fields
+ if "private_key" in fields:
+ result["private_key"] = private_key_hex
+ if "public_key" in fields:
+ result["public_key"] = public_key_hex
+ if "address" in fields:
+ result["address"] = address
+
+ data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result # Return the generated information as a dictionary
\ No newline at end of file
diff --git a/wallet_client.py b/wallet_client.py
index 39c08e4..b864915 100644
--- a/wallet_client.py
+++ b/wallet_client.py
@@ -7,9 +7,13 @@
import sys
import threading
import gc
-from collections import Counter, OrderedDict
import re
import time
+from datetime import datetime
+
+from decimal import Decimal, ROUND_DOWN
+import requests
+from collections import Counter, OrderedDict
# Get the absolute path of the directory containing the current script.
dir_path = os.path.dirname(os.path.realpath(__file__))
@@ -17,10 +21,16 @@
# Insert folder paths for modules
sys.path.insert(0, dir_path + "/denaro")
sys.path.insert(0, dir_path + "/denaro/wallet")
-from denaro.key_generation import generate
-from denaro.wallet.cryptographic_util import VerificationUtils, CryptoWallet, TOTP_Utils, DataManipulation
-from denaro.wallet.interface_util import QRCodeUtils, UserPrompts
+sys.path.insert(0, dir_path + "/denaro/wallet/utils")
+from denaro.wallet.utils.wallet_generation_util import generate, generate_from_private_key, string_to_point, sha256
+from denaro.wallet.utils.cryptographic_util import EncryptDecryptUtils, TOTP
+from denaro.wallet.utils.verification_util import Verification
+from denaro.wallet.utils.data_manipulation_util import DataManipulation
+from denaro.wallet.utils.interface_util import QRCodeUtils, UserPrompts
+from denaro.wallet.utils.transaction_utils.transaction_input import TransactionInput
+from denaro.wallet.utils.transaction_utils.transaction_output import TransactionOutput
+from denaro.wallet.utils.transaction_utils.transaction import Transaction
is_windows = os.name == 'nt'
@@ -131,7 +141,7 @@ def _load_data(filename, new_wallet):
# raise
# Wallet Helper Functions
-def generate_encrypted_wallet_data(wallet_data, current_data, password, totp_secret, hmac_salt, verification_salt, stored_verifier):
+def generate_encrypted_wallet_data(wallet_data, current_data, password, totp_secret, hmac_salt, verification_salt, stored_verifier, is_import=False):
"""Overview:
The `generate_encrypted_wallet_data` function serves as a utility for constructing a fully encrypted representation
of the wallet's data. It works by individually encrypting fields like private keys or mnemonics and then organizing
@@ -151,13 +161,13 @@ def generate_encrypted_wallet_data(wallet_data, current_data, password, totp_sec
"""
# Encrypt the wallet's private key
encrypted_wallet_data = {
- "id": CryptoWallet.encrypt_data(str(len(current_data["wallet_data"]["entry_data"]["entries"]) + 1), password, totp_secret, hmac_salt, verification_salt, stored_verifier),
- "private_key": CryptoWallet.encrypt_data(wallet_data['private_key'], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ "id": EncryptDecryptUtils.encrypt_data(str(len(current_data["wallet_data"]["entry_data"]["entries"] if not is_import else current_data["wallet_data"]["entry_data"]["imported_entries"]) + 1), password, totp_secret, hmac_salt, verification_salt, stored_verifier),
+ "private_key": EncryptDecryptUtils.encrypt_data(wallet_data['private_key'], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
}
# If the wallet is non-deterministic, encrypt the mnemonic
- if current_data["wallet_data"]["wallet_type"] == "non-deterministic":
- encrypted_wallet_data["mnemonic"] = CryptoWallet.encrypt_data(wallet_data['mnemonic'], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ if current_data["wallet_data"]["wallet_type"] == "non-deterministic" and not is_import:
+ encrypted_wallet_data["mnemonic"] = EncryptDecryptUtils.encrypt_data(wallet_data['mnemonic'], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
del encrypted_wallet_data["private_key"]
# Ensure a specific order for the keys
desired_key_order = ["id", "mnemonic"]
@@ -167,7 +177,7 @@ def generate_encrypted_wallet_data(wallet_data, current_data, password, totp_sec
DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
-def generate_unencrypted_wallet_data(wallet_data, current_data):
+def generate_unencrypted_wallet_data(wallet_data, current_data, is_import=False):
"""Overview:
Contrasting its encrypted counterpart, the `generate_unencrypted_wallet_data` function focuses on constructing
plaintext wallet data entries. While it doesn't encrypt data, it organizes the it in a structured manner, ensuring
@@ -183,13 +193,13 @@ def generate_unencrypted_wallet_data(wallet_data, current_data):
"""
# Structure the data without encryption
unencrypted_wallet_data = {
- "id": str(len(current_data["wallet_data"]["entry_data"]["entries"]) + 1),
+ "id": str(len(current_data["wallet_data"]["entry_data"]["entries"] if not is_import else current_data["wallet_data"]["entry_data"]["imported_entries"]) + 1),
"private_key": wallet_data['private_key'],
"public_key": wallet_data['public_key'],
"address": wallet_data['address']
}
# For non-deterministic wallets, include the mnemonic
- if current_data["wallet_data"]["wallet_type"] == "non-deterministic":
+ if current_data["wallet_data"]["wallet_type"] == "non-deterministic" and not is_import:
unencrypted_wallet_data["mnemonic"] = wallet_data['mnemonic']
# Ensure a specific order for the keys
desired_key_order = ["id", "mnemonic", "private_key", "public_key", "address"]
@@ -252,7 +262,7 @@ def handle_new_encrypted_wallet(password, totp_code, use2FA, filename, determini
data["wallet_data"]["verification_salt"] = base64.b64encode(verification_salt).decode()
# Hash the password with the salt for verification
- verifier = VerificationUtils.hash_password(password, verification_salt)
+ verifier = Verification.hash_password(password, verification_salt)
data["wallet_data"]["verifier"] = base64.b64encode(verifier).decode('utf-8')
# If no TOTP code is provided, set it to an empty string
@@ -263,7 +273,7 @@ def handle_new_encrypted_wallet(password, totp_code, use2FA, filename, determini
if use2FA:
#global close_qr_window
# Generate a secret for TOTP
- totp_secret = TOTP_Utils.generate_totp_secret(False,verification_salt)
+ totp_secret = TOTP.generate_totp_secret(False,verification_salt)
totp_qr_data = f'otpauth://totp/{filename}?secret={totp_secret}&issuer=Denaro Wallet Client'
# Generate a QR code for the TOTP secret
@@ -273,7 +283,7 @@ def handle_new_encrypted_wallet(password, totp_code, use2FA, filename, determini
thread.start()
# Encrypt the TOTP secret for storage
- encrypted_totp_secret = CryptoWallet.encrypt_data(totp_secret, password, "", hmac_salt, verification_salt, verifier)
+ encrypted_totp_secret = EncryptDecryptUtils.encrypt_data(totp_secret, password, "", hmac_salt, verification_salt, verifier)
data["wallet_data"]["totp_secret"] = encrypted_totp_secret
# Validate the TOTP setup
@@ -286,8 +296,8 @@ def handle_new_encrypted_wallet(password, totp_code, use2FA, filename, determini
thread.join()
else:
# If 2FA is not used, generate a predictable TOTP secret based on the verification salt.
- totp_secret = TOTP_Utils.generate_totp_secret(True,verification_salt)
- encrypted_totp_secret = CryptoWallet.encrypt_data(totp_secret, password, "", hmac_salt, verification_salt, verifier)
+ totp_secret = TOTP.generate_totp_secret(True,verification_salt)
+ encrypted_totp_secret = EncryptDecryptUtils.encrypt_data(totp_secret, password, "", hmac_salt, verification_salt, verifier)
data["wallet_data"]["totp_secret"] = encrypted_totp_secret
totp_secret = ""
@@ -325,10 +335,11 @@ def handle_existing_encrypted_wallet(filename, data, password, totp_code, determ
hmac_salt = base64.b64decode(data["wallet_data"]["hmac_salt"])
# Verify the password and HMAC
- password_verified, hmac_verified, stored_verifier = VerificationUtils.verify_password_and_hmac(data, password, hmac_salt, verification_salt, deterministic)
+ password_verified, hmac_verified, stored_verifier = Verification.verify_password_and_hmac(data, password, hmac_salt, verification_salt, deterministic)
# Based on password verification, update or reset the number of failed attempts
data = DataManipulation.update_or_reset_attempts(data, filename, hmac_salt, password_verified, deterministic)
+
if data is None:
DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return None, None, None, None
@@ -336,7 +347,7 @@ def handle_existing_encrypted_wallet(filename, data, password, totp_code, determ
DataManipulation._save_data(filename,data)
# Verify the password and HMAC
- password_verified, hmac_verified, stored_verifier = VerificationUtils.verify_password_and_hmac(data, password, hmac_salt, verification_salt, deterministic)
+ password_verified, hmac_verified, stored_verifier = Verification.verify_password_and_hmac(data, password, hmac_salt, verification_salt, deterministic)
# Fail if either the password or HMAC verification failed
if not (password_verified and hmac_verified):
@@ -345,7 +356,7 @@ def handle_existing_encrypted_wallet(filename, data, password, totp_code, determ
return None, None, None, None
# If 2FA is enabled, handle the TOTP validation
- totp_secret, tfa_enabled = VerificationUtils.verify_totp_secret(password, data["wallet_data"]["totp_secret"], hmac_salt, verification_salt, stored_verifier)
+ totp_secret, tfa_enabled = Verification.verify_totp_secret(password, data["wallet_data"]["totp_secret"], hmac_salt, verification_salt, stored_verifier)
if tfa_enabled:
tfa_valid = UserPrompts.handle_2fa_validation(totp_secret, totp_code)
if not tfa_valid or not tfa_valid.get("valid"):
@@ -384,10 +395,10 @@ def parse_and_encrypt_mnemonic(words, password, totp_secret, hmac_salt, verifica
# Encrypt each word, and structure it in a dictionary with its ID
encrypted_key_data = [
- CryptoWallet.encrypt_data(
+ EncryptDecryptUtils.encrypt_data(
json.dumps({
- "id": CryptoWallet.encrypt_data(str(i+1), password, totp_secret, hmac_salt, verification_salt, stored_verifier),
- "word": CryptoWallet.encrypt_data(word, password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ "id": EncryptDecryptUtils.encrypt_data(str(i+1), password, totp_secret, hmac_salt, verification_salt, stored_verifier),
+ "word": EncryptDecryptUtils.encrypt_data(word, password, totp_secret, hmac_salt, verification_salt, stored_verifier)
}),
password, totp_secret, hmac_salt, verification_salt, stored_verifier
) for i, word in enumerate(word_list)
@@ -414,8 +425,8 @@ def decrypt_and_parse_mnemonic(encrypted_json, password, totp_secret, hmac_salt,
- str: A string containing the decrypted sequence of mnemonic words.
"""
decrypted_words = [
- CryptoWallet.decrypt_data(
- json.loads(CryptoWallet.decrypt_data(encrypted_index, password, totp_secret, hmac_salt, verification_salt, stored_verifier))["word"],
+ EncryptDecryptUtils.decrypt_data(
+ json.loads(EncryptDecryptUtils.decrypt_data(encrypted_index, password, totp_secret, hmac_salt, verification_salt, stored_verifier))["word"],
password, totp_secret, hmac_salt, verification_salt, stored_verifier
) for encrypted_index in encrypted_json
]
@@ -424,7 +435,7 @@ def decrypt_and_parse_mnemonic(encrypted_json, password, totp_secret, hmac_salt,
return result
# Wallet Orchestrator Functions
-def generateAddressHelper(filename, password, totp_code=None, new_wallet=False, encrypt=False, use2FA=False, deterministic=False,backup=None,disable_warning=False,overwrite_password=None):
+def generateAddressHelper(filename, password, totp_code=None, new_wallet=False, encrypt=False, use2FA=False, deterministic=False,backup=None,disable_warning=False,overwrite_password=None, amount=1, private_key=None, is_import=False):
"""Overview:
The `generateAddressHelper` function serves as a central orchestrator for facilitating the creation,
integration, and management of wallet data. This function is designed to accomodate different scenarios
@@ -465,7 +476,8 @@ def generateAddressHelper(filename, password, totp_code=None, new_wallet=False,
- str: A string that represents a newly generated address.
"""
# Initialize mnemonic to None
- mnemonic = None
+ mnemonic = None
+
#Make sure that the wallet directories exists
ensure_wallet_directories_exist()
@@ -476,6 +488,7 @@ def generateAddressHelper(filename, password, totp_code=None, new_wallet=False,
data, wallet_exists = _load_data(filename, new_wallet)
# If wallet dose not exist return None
+ # This handles the case if using generateaddress for a wallet that dose not exist
if not new_wallet and not wallet_exists:
DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return None
@@ -483,26 +496,36 @@ def generateAddressHelper(filename, password, totp_code=None, new_wallet=False,
if new_wallet:
stored_encrypt_param = encrypt
stored_deterministic_param = deterministic
+
+ imported_entries = 0
# Determine encryption status and wallet type for an existing wallet
if wallet_exists or not new_wallet:
# Convert part of the wallet data to a JSON string
- data_segment = json.dumps(data["wallet_data"])
+ data_segment = json.dumps(data["wallet_data"])
+
# Check if the wallet data is encrypted and if a password is provided
if is_wallet_encrypted(data_segment) and not password and not new_wallet:
- logging.error("Wallet is encrypted. Password is required to add additional addresses.")
+ logging.error("Wallet is encrypted. A password is required to add additional addresses.")
DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return None
+
if is_wallet_encrypted(data_segment):
# If encrypted and password is provided, set encrypt flag to True
encrypt = True
+
if not is_wallet_encrypted(data_segment):
encrypt = False
+
# Check if the existing wallet type is deterministic
if "wallet_type" in data["wallet_data"] and not new_wallet:
deterministic = data["wallet_data"]["wallet_type"] == "deterministic"
- if len(data["wallet_data"]["entry_data"]["entries"]) > 255 and not new_wallet:
- logging.info("Cannot proceed. Max wallet entries reached.")
+
+ if "imported_entries" in data["wallet_data"]["entry_data"]:
+ imported_entries = len(data["wallet_data"]["entry_data"]["imported_entries"])
+
+ if len(data["wallet_data"]["entry_data"]["entries"]) + imported_entries > 255 and not new_wallet:
+ print("Cannot proceed. Maximum wallet entries reached.")
return None
#Handle backup and overwrite for an existing wallet
@@ -517,14 +540,17 @@ def generateAddressHelper(filename, password, totp_code=None, new_wallet=False,
print()
if new_wallet:
+ logging.info("new_wallet is set to True.")
encrypt = stored_encrypt_param
deterministic = stored_deterministic_param
+ else:
+ logging.info("new_wallet is set to False.")
+
# Handle different scenarios based on whether the wallet is encrypted
- if encrypt:
+ if encrypt:
+ logging.info("encrypt is set to True.")
if new_wallet:
- logging.info("new_wallet is set to True")
- logging.info("encrypt is set to True.")
- logging.info("Handling new encrypted wallet...")
+ logging.info("Handling new encrypted wallet.")
# Handle creation of a new encrypted wallet
data, totp_secret, hmac_salt, verification_salt, stored_verifier = handle_new_encrypted_wallet(password, totp_code, use2FA, filename, deterministic)
if not data:
@@ -532,119 +558,240 @@ def generateAddressHelper(filename, password, totp_code=None, new_wallet=False,
DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return None
else:
- logging.info("new_wallet is set to False")
- logging.info("Existing wallet is encrypted.")
- logging.info("Handling existing encrypted wallet...")
+ logging.info("Handling existing encrypted wallet.")
# Handle operations on an existing encrypted wallet
hmac_salt, verification_salt, stored_verifier, totp_secret = handle_existing_encrypted_wallet(filename, data, password, totp_code, deterministic)
if not hmac_salt or not verification_salt or not stored_verifier:
#logging.error(f"Error: Data from handle_existing_encrypted_wallet is None!\nDebug: HMAC Salt: {hmac_salt}, Verification Salt: {verification_salt}, Stored Verifier: {stored_verifier}")
DataManipulation.secure_delete([var for var in locals().values() if var is not None])
return None
-
- # If deterministic flag is set, generate addresses in a deterministic way
- if deterministic:
- if not password:
- logging.error("Password is required to derive the deterministic address")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- return None
- if new_wallet:
- logging.info("deterministic is set to True")
- logging.info("Generating wallet data")
- # Generate the initial data for a new deterministic wallet
- wallet_data = generate(passphrase=password,deterministic=True)
- if encrypt:
- logging.info("Parseing and encrypting master mnemonic")
- # Parse and encrypt the mnemonic words individually
- data["wallet_data"]["entry_data"]["key_data"] = parse_and_encrypt_mnemonic(wallet_data["mnemonic"], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ else:
+ logging.info("encrypt is set to False.")
+
+ logging.info(f"is_import is set to {is_import}")
+
+ # Check if the user is importing a wallet entry
+ if not is_import:
+ # If deterministic flag is set, generate addresses in a deterministic way
+ if deterministic:
+ logging.info("deterministic is set to True.")
+ if not password:
+ logging.error("Password is required to derive the deterministic address.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+ if new_wallet:
+ logging.info("Generating deterministic wallet data.")
+ # Generate the initial data for a new deterministic wallet
+ wallet_data = generate(passphrase=password,deterministic=True)
+ if encrypt:
+ logging.info("Data successfully generated for new encrypted deterministic wallet.")
+ logging.info("Parseing and encrypting master mnemonic.")
+ # Parse and encrypt the mnemonic words individually
+ data["wallet_data"]["entry_data"]["key_data"] = parse_and_encrypt_mnemonic(wallet_data["mnemonic"], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ else:
+ logging.info("Data successfully generated for new unencrypted deterministic wallet.")
+ # Structure for a new unencrypted deterministic wallet
+ data = {
+ "wallet_data": {
+ "wallet_type": "deterministic",
+ "version": "0.2.2",
+ "entry_data": {
+ "master_mnemonic": wallet_data["mnemonic"],
+ "entries":[]
+ }
+ }
+ }
+ else:
+ # Set the deterministic index value based on the length of the entries in the wallet
+ index = len(data["wallet_data"]["entry_data"]["entries"])
+ if encrypt:
+ logging.info("Decrypting and parsing the master mnemonic.")
+ # Decrypt and parse the existing mnemonic for the deterministic wallet
+ mnemonic = decrypt_and_parse_mnemonic(data["wallet_data"]["entry_data"]["key_data"], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ logging.info("Master mnemonic successfully decrypted.")
+ wallet_data = []
+ entries_generated = -1
+ logging.info("Generating deterministic wallet data.")
+ for _ in range(amount):
+ if index + entries_generated < 256:
+ entries_generated += 1
+ generated_data = generate(mnemonic_phrase=mnemonic, passphrase=password, index=index+entries_generated, deterministic=True)
+ wallet_data.append(generated_data)
+ if index + len(wallet_data) >= 256:
+ print("Maximum wallet entries reached.\n")
+ break
+ logging.info(f"{entries_generated + 1} address(es) successfully generated for existing encrypted determinsitic wallet.")
+ else:
+ # Use the existing mnemonic directly if it's not encrypted
+ mnemonic = data["wallet_data"]["entry_data"]["master_mnemonic"]
+ logging.info("Validating passphrase used for address derivation.")
+ # Verify if the provided passphrase correctly derives child keys.
+ # Derive the first child key using the master mnemonic and the given passphrase.
+ first_child_data = generate(mnemonic_phrase=mnemonic,passphrase=password, index=0, deterministic=True)
+ # Check if the derived child's private key matches the private key of the first entry in the stored wallet.
+ if first_child_data["private_key"] != data["wallet_data"]["entry_data"]["entries"][0]["private_key"]:
+ # Log an error message if the private keys do not match, indicating that the provided passphrase is incorrect.
+ logging.error("Invalid passphrase. To derive the deterministic address, please re-enter the correct passphrase and try again.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+ else:
+ logging.info("Passphrase validated.")
+ wallet_data = []
+ entries_generated = -1
+ logging.info("Generating deterministic wallet data.")
+ for _ in range(amount):
+ if index + entries_generated < 256:
+ entries_generated += 1
+ generated_data = generate(mnemonic_phrase=mnemonic, passphrase=password, index=index + entries_generated, deterministic=True)
+ wallet_data.append(generated_data)
+ if index + len(wallet_data) >= 256:
+ print("Maximum wallet entries reached.\n")
+ break
+ logging.info(f"{entries_generated + 1} address(es) successfully generated for existing unencrypted determinsitic wallet.")
+ else:
+ logging.info("deterministic is set to False")
+ # For non-deterministic wallets, generate a random wallet data
+ if not new_wallet:
+ wallet_data = []
+ entries_generated = -1
+ logging.info("Generating non-deterministic wallet data.")
+ for _ in range(amount):
+ if len(data["wallet_data"]["entry_data"]["entries"]) < 256:
+ generated_data = generate()
+ wallet_data.append(generated_data)
+ entries_generated += 1
+ if len(data["wallet_data"]["entry_data"]["entries"]) + len(wallet_data) >= 256:
+ print("Maximum wallet entries reached.\n")
+ break
+ if encrypt:
+ logging.info(f"{entries_generated + 1} address(es) successfully generated for existing encrypted non-determinsitic wallet.")
+ else:
+ logging.info(f"{entries_generated + 1} address(es) successfully generated for existing unencrypted non-determinsitic wallet.")
else:
- logging.info("encrypt is set to False")
- logging.info("Generating data for new unencrypted deterministic wallet...")
- # Structure for a new unencrypted deterministic wallet
+ logging.info("Generating non-deterministic wallet data.")
+ wallet_data = generate()
+ if new_wallet and not encrypt:
data = {
"wallet_data": {
- "wallet_type": "deterministic",
+ "wallet_type": "non-deterministic",
"version": "0.2.2",
"entry_data": {
- "master_mnemonic": wallet_data["mnemonic"],
"entries":[]
}
+
}
}
- else:
- # Set the deterministic index value based on the length of the entries in the wallet
+ logging.info("Data successfully generated for new unencrypted non-deterministic wallet.")
+ if new_wallet and encrypt:
+ logging.info("Data successfully generated for new encrypted non-deterministic wallet.")
+ else:
+ if not new_wallet:
+ # Initialize wallet_data dictionary
+ wallet_data = []
+
+ # Get number of wallet entries
index = len(data["wallet_data"]["entry_data"]["entries"])
- if encrypt:
- logging.info("Decrypting and parsing the master mnemonic")
- # Decrypt and parse the existing mnemonic for the deterministic wallet
- mnemonic = decrypt_and_parse_mnemonic(data["wallet_data"]["entry_data"]["key_data"], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
- wallet_data = generate(mnemonic_phrase=mnemonic,passphrase=password, index=index, deterministic=True)
- else:
- logging.info("encrypt is set to False")
- logging.info("Generating child address for existing unencrypted deterministic wallet...")
- # Use the existing mnemonic directly if it's not encrypted
- mnemonic = data["wallet_data"]["entry_data"]["master_mnemonic"]
- # Verify if the provided passphrase correctly derives child keys.
- # Derive the first child key using the master mnemonic and the given passphrase.
- first_child_data = generate(mnemonic_phrase=mnemonic,passphrase=password, index=0, deterministic=True)
- # Check if the derived child's private key matches the private key of the first entry in the stored wallet.
- if first_child_data["private_key"] != data["wallet_data"]["entry_data"]["entries"][0]["private_key"]:
- # Log an error message if the private keys do not match, indicating that the provided passphrase is incorrect.
- logging.error("Invalid passphrase. To derive the deterministic address, please re-enter the correct passphrase and try again.")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- return None
- else:
- # Generate the new address based on the existing mnemonic
- wallet_data = generate(mnemonic_phrase=mnemonic,passphrase=password, index=index, deterministic=True)
- else:
- logging.info("Deterministic is set to False")
- # For non-deterministic wallets, generate a random wallet data
- wallet_data = generate()
- #print(wallet_data)
- if new_wallet and not encrypt:
- logging.info("new_wallet is set to True")
- logging.info("encrypt is set to False")
- logging.info("Generating data for new unencrypted non-deterministic wallet...")
- data = {
- "wallet_data": {
- "wallet_type": "non-deterministic",
- "version": "0.2.2",
- "entry_data": {
- "entries":[]
- }
-
- }
- }
+ # Return None if amount of wallet entries are 256 or more
+ if len(data["wallet_data"]["entry_data"]["entries"]) >= 256:
+ logging.error("Maximum wallet entries reached.\n")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ # Validate private key using regex pattern
+ private_key_pattern = r'^(0x)?[0-9a-fA-F]{64}$'
+ if not re.match(private_key_pattern, private_key):
+ logging.error("The private key provided is not valid.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ # Remove 0x prefix from private key if it exists
+ if private_key.startswith('0x'):
+ private_key = private_key[2:]
+
+ logging.info("Generating import data based on the provided private key.")
+
+ # Generate wallet data from private key
+ generated_data = generate_from_private_key(private_key_hex=private_key)
+
+ if generated_data:
+ logging.info("Data successfully generated from private key.")
+
+ # Ensure imported_entries exists
+ if not "imported_entries" in data["wallet_data"]["entry_data"]:
+ data["wallet_data"]["entry_data"]["imported_entries"] = []
+
+ # Append generated data to wallet_data
+ wallet_data.append(generated_data)
# Prepare data to be saved based on encryption status
if encrypt:
- # If the wallet is encrypted, encrypt the new address before saving
- encrypted_wallet_data = generate_encrypted_wallet_data(wallet_data, data, password, totp_secret, hmac_salt, verification_salt, stored_verifier)
- encrypted_data_entry = CryptoWallet.encrypt_data(json.dumps(encrypted_wallet_data), password, totp_secret, hmac_salt, verification_salt, stored_verifier)
- data["wallet_data"]["entry_data"]["entries"].append(encrypted_data_entry)
-
+ # Prepare encrypted data to be saved
+ logging.info("Encrypting generated data.")
+ if new_wallet:
+ encrypted_wallet_data = generate_encrypted_wallet_data(wallet_data, data, password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ encrypted_data_entry = EncryptDecryptUtils.encrypt_data(json.dumps(encrypted_wallet_data), password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ data["wallet_data"]["entry_data"]["entries"].append(encrypted_data_entry)
+ else:
+ for item in wallet_data:
+ encrypted_wallet_data = generate_encrypted_wallet_data(item, data, password, totp_secret, hmac_salt, verification_salt, stored_verifier, is_import=is_import)
+ encrypted_data_entry = EncryptDecryptUtils.encrypt_data(json.dumps(encrypted_wallet_data), password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ if not is_import:
+ data["wallet_data"]["entry_data"]["entries"].append(encrypted_data_entry)
+ else:
+ data["wallet_data"]["entry_data"]["imported_entries"].append(encrypted_data_entry)
+
+ # Set HMAC message based on the encrypted wallet data
if deterministic:
hmac_msg = json.dumps(data["wallet_data"]["entry_data"]["entries"]).encode() + json.dumps(data["wallet_data"]["entry_data"]["key_data"]).encode()
else:
hmac_msg = json.dumps(data["wallet_data"]["entry_data"]["entries"]).encode()
+
+ if "imported_entries" in data["wallet_data"]["entry_data"]:
+ hmac_msg = json.dumps(data["wallet_data"]["entry_data"]["imported_entries"]).encode() + hmac_msg
# Calculate HMAC for wallet's integrity verification
- computed_hmac = VerificationUtils.hmac_util(password=password,hmac_salt=hmac_salt,hmac_msg=hmac_msg,verify=False)
+ computed_hmac = Verification.hmac_util(password=password,hmac_salt=hmac_salt,hmac_msg=hmac_msg,verify=False)
data["wallet_data"]["hmac"] = base64.b64encode(computed_hmac).decode()
else:
# Prepare unencrypted data to be saved
- unencrypted_data_entry = generate_unencrypted_wallet_data(wallet_data, data)
- data["wallet_data"]["entry_data"]["entries"].append(unencrypted_data_entry)
+ if new_wallet:
+ unencrypted_data_entry = generate_unencrypted_wallet_data(wallet_data, data)
+ data["wallet_data"]["entry_data"]["entries"].append(unencrypted_data_entry)
+ else:
+ for item in wallet_data:
+ unencrypted_data_entry = generate_unencrypted_wallet_data(item, data, is_import=is_import)
+ if not is_import:
+ data["wallet_data"]["entry_data"]["entries"].append(unencrypted_data_entry)
+ else:
+ data["wallet_data"]["entry_data"]["imported_entries"].append(unencrypted_data_entry)
# Save the updated wallet data back to the file
+ logging.info("Saving data to wallet file.")
DataManipulation._save_data(filename, data)
+
# Extract the newly generated address to be returned
- result = wallet_data['address']
+ if "-verbose" in sys.argv:
+ print("\n")
+ print("\033[2A")
+
+ # Sgie warning and other info to user
+ warning = 'WARNING: Never disclose your mnemonic phrase or private key! Anyone with access to these can steal the assets held in your account.'
+ if not is_import:
+ if amount == 1 and new_wallet:
+ result = f"Successfully generated new wallet.\n\n{warning}\n{'Master Mnemonic' if deterministic else 'Mnemonic'}: {wallet_data['mnemonic']}\nPrivate Key: 0x{wallet_data['private_key']}\nAddress #{len(data['wallet_data']['entry_data']['entries'])}: {wallet_data['address']}"
+ if amount == 1 and not new_wallet:
+ n ='\n'
+ result = f"Successfully generated and stored wallet entry.\n\n{warning}{n+'Mnemonic: ' + wallet_data[0]['mnemonic'] if not deterministic else ''}\nPrivate Key: 0x{wallet_data[0]['private_key']}\nAddress #{len(data['wallet_data']['entry_data']['entries'])}: {wallet_data[0]['address']}"
+ elif amount > 1 and not new_wallet:
+ result = f"Successfully generated and stored {entries_generated + 1} wallet entries."
+ else:
+ result = f"Successfully imported wallet entry.\n\n{warning}\nImported Private Key #{len(data['wallet_data']['entry_data']['imported_entries'])}: 0x{wallet_data[0]['private_key']}\nAddress: {wallet_data[0]['address']}"
+ print(result)
DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
return result
-def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=[], pretty=False):
+def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=[], pretty=False, show=None):
"""Overview:
The `decryptWalletEntries` function is designed to decrypt wallet entries stored within a specified file,
implementing an intricate decryption process with the collaboration of multiple helper functions.
@@ -710,9 +857,7 @@ def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=
# Convert the wallet data segment to a JSON string and check if it appears to be encrypted
data_segment = json.dumps(data["wallet_data"])
- if not is_wallet_encrypted(data_segment):
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- raise ValueError("Wallet data does not appear to be encrypted.")
+ is_encrypted = is_wallet_encrypted(data_segment)
# Initialize a flag to check if the wallet type is deterministic
deterministic = False
@@ -720,54 +865,119 @@ def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=
# Check if the wallet type is present in the data and set the deterministic flag accordingly
if "wallet_type" in data["wallet_data"]:
deterministic = data["wallet_data"]["wallet_type"] == "deterministic"
-
- # Extract necessary cryptographic salts and secrets for the encrypted wallet
- hmac_salt, verification_salt, stored_verifier, totp_secret = handle_existing_encrypted_wallet(filename,data, password, totp_code, deterministic)
- # Ensure none of the cryptographic values are missing
- if not hmac_salt or not verification_salt or not stored_verifier:
- #print(f"Error: Data from handle_existing_encrypted_wallet is None!\nDebug: HMAC Salt: {hmac_salt}, Verification Salt: {verification_salt}, Stored Verifier: {stored_verifier}")
- DataManipulation.secure_delete([var for var in locals().values() if var is not None])
- return None
+ index = len(data["wallet_data"]["entry_data"]["entries"])
+ imported_entries_length = 0
+ if "imported_entries" in data["wallet_data"]["entry_data"]:
+ imported_entries_length = len(data["wallet_data"]["entry_data"]["imported_entries"])
+
+ if is_encrypted:
+ # Extract necessary cryptographic salts and secrets for the encrypted wallet
+ hmac_salt, verification_salt, stored_verifier, totp_secret = handle_existing_encrypted_wallet(filename, data, password, totp_code, deterministic)
+
+ # Ensure none of the cryptographic values are missing
+ if not hmac_salt or not verification_salt or not stored_verifier:
+ #print(f"Error: Data from handle_existing_encrypted_wallet is None!\nDebug: HMAC Salt: {hmac_salt}, Verification Salt: {verification_salt}, Stored Verifier: {stored_verifier}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ # Handle warnings and messages
+ if 'send' in sys.argv:
+ print("\nA private key is required to send funds. \nSince a private key has not been provided, the wallet client will attempt to decrypt each entry in the wallet file until it finds the private key associated with the address specified. \nYou can use the '-private-key' argument to make this process alot faster. However, doing this is not secure and can put your funds at risk.\n")
+
+ if index + imported_entries_length >= 32:
+ logging.warning(f"The encrypted wallet file contains {index} entries and is quite large. Decryption {'and balance requests ' if 'balance' in sys.argv else ''}may take a while.\n")
+ else:
+ if index + imported_entries_length >= 32 and 'balance' in sys.argv:
+ logging.warning(f"The wallet file contains {index} entries and and is quite large. Balance requests for entire wallets may take a while.\n")
+
# If the wallet is deterministic, decrypt and parse the mnemonic phrase
if deterministic:
- mnemonic = decrypt_and_parse_mnemonic(data["wallet_data"]["entry_data"]["key_data"], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ if is_encrypted:
+ mnemonic = decrypt_and_parse_mnemonic(data["wallet_data"]["entry_data"]["key_data"], password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ else:
+ mnemonic = data["wallet_data"]["entry_data"]["master_mnemonic"]
# List to hold decrypted wallet entries
- decrypted_entries = []
+ decrypted_entries = []
# If no fields are specified then all fields are considered
if fields == []:
- fields = ["mnemonic", "id", "private_key", "public_key", "address"]
-
- # Decrypt each entry in the wallet data
- for encrypted_entry in data["wallet_data"]["entry_data"]["entries"]:
- entry_with_encrypted_values = json.loads(CryptoWallet.decrypt_data(encrypted_entry, password, totp_secret, hmac_salt, verification_salt, stored_verifier))
+ fields = ["mnemonic", "id", "private_key", "public_key", "address","is_import"]
+
+ entry_count = 0
+ address_found = False
+ is_import = False
+
+ if show:
+ if "imported" in show:
+ if "imported_entries" in data["wallet_data"]["entry_data"]:
+ index = 0
+ del data["wallet_data"]["entry_data"]["entries"]
- fully_decrypted_entry = {}
- for key, encrypted_value in entry_with_encrypted_values.items():
- fully_decrypted_entry[key] = CryptoWallet.decrypt_data(encrypted_value, password, totp_secret, hmac_salt, verification_salt, stored_verifier)
-
- # Generate required data fields based on the mnemonic phrase and deterministic flag
- generated_data = {}
- if not deterministic:
- generated_data = generate(mnemonic_phrase=fully_decrypted_entry["mnemonic"], deterministic=deterministic,fields=fields)
- if address and not "address" in fields:
- generated_data.update(generate(mnemonic_phrase=fully_decrypted_entry["mnemonic"], deterministic=deterministic,fields=["address"]))
- else:
- generated_data = generate(mnemonic_phrase=mnemonic, passphrase=password, index=int(fully_decrypted_entry["id"]) - 1, deterministic=deterministic,fields=fields)
- if not "id" in fields:
- generated_data.update(generate(mnemonic_phrase=mnemonic, passphrase=password, index=int(fully_decrypted_entry["id"]) - 1, deterministic=deterministic,fields=["id"]))
- if address and not "address" in fields:
- generated_data.update(generate(mnemonic_phrase=mnemonic, passphrase=password, index=int(fully_decrypted_entry["id"]) - 1, deterministic=deterministic,fields=["address"]))
- generated_data["id"] = int(generated_data["id"]) + 1
- if "mnemonic" in generated_data:
- del generated_data["mnemonic"]
-
- # Update the decrypted entry with the generated data
- fully_decrypted_entry.update(generated_data)
- decrypted_entries.append(fully_decrypted_entry)
+ if "generated" in show:
+ if "imported_entries" in data["wallet_data"]["entry_data"]:
+ imported_entries_length = 0
+ del data["wallet_data"]["entry_data"]["imported_entries"]
+
+ for entry_data in data["wallet_data"]["entry_data"]:
+ if entry_data != "key_data" and entry_data != "master_mnemonic":
+ for entry in data["wallet_data"]["entry_data"][entry_data]:
+ if entry_data == "imported_entries":
+ is_import = True
+ if is_encrypted:
+ entry_count += 1
+ # If wallet is encrypted, decrypt each entry in the wallet data
+ entry_with_encrypted_values = json.loads(EncryptDecryptUtils.decrypt_data(entry, password, totp_secret, hmac_salt, verification_salt, stored_verifier))
+ fully_decrypted_entry = {}
+ for key, encrypted_value in entry_with_encrypted_values.items():
+ fully_decrypted_entry[key] = EncryptDecryptUtils.decrypt_data(encrypted_value, password, totp_secret, hmac_salt, verification_salt, stored_verifier)
+ # Generate required data fields based on the mnemonic phrase and deterministic flag
+ generated_data = {}
+ if not is_import:
+ if not deterministic:
+ generated_data = generate(mnemonic_phrase=fully_decrypted_entry["mnemonic"], deterministic=deterministic,fields=fields)
+ if address and not "address" in fields:
+ generated_data.update(generate(mnemonic_phrase=fully_decrypted_entry["mnemonic"], deterministic=deterministic,fields=["address"]))
+ else:
+ generated_data = generate(mnemonic_phrase=mnemonic, passphrase=password, index=int(fully_decrypted_entry["id"]) - 1, deterministic=deterministic,fields=fields)
+ if not "id" in fields:
+ generated_data.update(generate(mnemonic_phrase=mnemonic, passphrase=password, index=int(fully_decrypted_entry["id"]) - 1, deterministic=deterministic,fields=["id"]))
+ if address and not "address" in fields:
+ generated_data.update(generate(mnemonic_phrase=mnemonic, passphrase=password, index=int(fully_decrypted_entry["id"]) - 1, deterministic=deterministic,fields=["address"]))
+ generated_data["id"] = int(generated_data["id"]) + 1
+ if "mnemonic" in generated_data:
+ del generated_data["mnemonic"]
+ else:
+ generated_data = generate_from_private_key(private_key_hex=fully_decrypted_entry["private_key"])
+ generated_data["is_import"] = is_import
+ # Update the decrypted entry with the generated data
+ fully_decrypted_entry.update(generated_data)
+ if 'send' in sys.argv:
+ print(f"\rDecrypting wallet entry {entry_count} of {index + imported_entries_length} | Address: {generated_data['address']}", end='')
+ if address[0] in fully_decrypted_entry['address']:
+ print("\nAddress Found.\n")
+ decrypted_entries = []
+ decrypted_entries.append(fully_decrypted_entry)
+ address_found = True
+ break
+ else:
+ decrypted_entries = []
+ else:
+ print(f"\rDecrypting wallet entry {entry_count} of {index + imported_entries_length}", end='')
+ else:
+ fully_decrypted_entry = {}
+ for key, value in entry.items():
+ fully_decrypted_entry[key] = value
+ if is_import:
+ fully_decrypted_entry["is_import"] = is_import
+ decrypted_entries.append(fully_decrypted_entry)
+ if 'send' in sys.argv and address_found:
+ break
+
+ if is_encrypted and not address_found:
+ print("\n")
# Initialize variables to hold addresses not found for inclusion and exclusion
not_found_inclusion = []
@@ -782,7 +992,7 @@ def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=
# Existing decrypted addresses
all_decrypted_addresses = [entry.get("address") for entry in decrypted_entries]
-
+
# If an address is specified, filter the decrypted entries based on that address
if address:
for addr in address:
@@ -796,7 +1006,7 @@ def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=
unique_filtered_entries.extend(filtered_entries)
else:
not_found_inclusion.append(addr)
-
+
# Filter logic for address exclusion
if addresses_to_exclude:
unique_filtered_entries = [entry for entry in unique_filtered_entries if entry.get("address") not in addresses_to_exclude]
@@ -807,26 +1017,34 @@ def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=
# Remove duplicate and excluded addresses from unique_filtered_entries
seen_addresses = set()
unique_filtered_entries = [entry for entry in unique_filtered_entries if entry['address'] not in (seen_addresses or addresses_to_exclude) and not seen_addresses.add(entry['address'])]
-
+
+ showed_warning = False
# Check if there are any addresses not found for inclusion or exclusion
if not_found_inclusion or not_found_exclusion:
not_found_all = list(set(not_found_inclusion + not_found_exclusion))
not_found_all.sort(key=lambda x: address.index(x) if x in address else address.index('-' + x))
- print(f"Warning: The following {'address was' if len(not_found_all) == 1 else 'addresses were'} not found: {', '.join(not_found_all)}")
+ if not 'send' in sys.argv:
+ logging.warning(f"The following {'address is' if len(not_found_all) == 1 else 'addresses are'} not associated with this wallet: {', '.join(not_found_all)}\n")
+ showed_warning = True
+ else:
+ logging.error(f"The address specified is not associated with this wallet.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
# Error logic
if not unique_filtered_entries:
- if all(addr in addresses_to_exclude for addr in all_decrypted_addresses):
- print("All specified addresses are excluded. Returning no entries.")
- else:
- print(f"Error: {'The address specified is not' if len(address) == 1 else 'None of the addresses specified are'} associated with this wallet. Returning all wallet entries...")
- # Sort and return unique_filtered_entries
+ if not 'send' in sys.argv and not showed_warning:
+ if all(addr in addresses_to_exclude for addr in all_decrypted_addresses):
+ logging.error("All of the addresses associated with the wallet have been excluded. There is nothing to return.\n")# Returning no entries.")
+ else:
+ logging.error(f"{'The address specified is not' if len(address) == 1 else 'None of the addresses specified are'} associated with this wallet.\n")# Returning all wallet entries...")
else:
- unique_filtered_entries.sort(key=lambda x: x['id'])
+ # Sort and return unique_filtered_entries
+ unique_filtered_entries.sort(key=lambda x: int(x['id']))
decrypted_entries = unique_filtered_entries
-
+
# Specify the desired order of fields for output
- ordered_field_names = ["id", "mnemonic", "private_key", "public_key", "address"]
+ ordered_field_names = ["id", "mnemonic", "private_key", "public_key", "address", "is_import"]
# If specific fields are requested, filter and order the decrypted entries based on those fields
if fields:
@@ -834,28 +1052,537 @@ def decryptWalletEntries(filename, password, totp_code=None, address=[], fields=
else:
# Ensure the order of fields in the output, even if no specific fields are requested
decrypted_entries = [OrderedDict((field, entry[field]) for field in ordered_field_names if field in entry) for entry in decrypted_entries]
-
+
+ imported_entries = []
+ for entry in decrypted_entries:
+ if "is_import" in entry:
+ imported_entries.append(entry)
+
+ for entry in imported_entries:
+ if entry in decrypted_entries:
+ del entry["is_import"]
+ decrypted_entries.remove(entry)
+
# Convert the decrypted entries to a readable format based on the `pretty` flag
if pretty:
if "mnemonic" in fields and deterministic:
formatted_output = json.dumps({"entry_data":{"master_mnemonic": mnemonic, "entries": decrypted_entries}}, indent=4)
+ if len(imported_entries) > 0:
+ formatted_output = json.dumps({"entry_data":{"master_mnemonic": mnemonic, "entries": decrypted_entries, "imported_entries": imported_entries}}, indent=4)
else:
formatted_output = json.dumps({"entry_data":{"entries": decrypted_entries}}, indent=4)
+ if len(imported_entries) > 0:
+ formatted_output = json.dumps({"entry_data":{"entries": decrypted_entries, "imported_entries": imported_entries}}, indent=4)
else:
if "mnemonic" in fields and deterministic:
formatted_output = json.dumps({"entry_data":{"master_mnemonic": mnemonic, "entries": decrypted_entries}})
+ if len(imported_entries) > 0:
+ formatted_output = json.dumps({"entry_data":{"master_mnemonic": mnemonic, "entries": decrypted_entries, "imported_entries": imported_entries}})
else:
formatted_output = json.dumps({"entry_data":{"entries": decrypted_entries}})
+ if len(imported_entries) > 0:
+ formatted_output = json.dumps({"entry_data":{"entries": decrypted_entries, "imported_entries": imported_entries}})
+
+ # Convert JSON string back to dictionary
+ formatted_output = json.loads(formatted_output)
+
+ # Remove 'entries' key if empty and imported entries are only being returned
+ if show == "imported" and formatted_output["entry_data"].get("entries") == []:
+ del formatted_output["entry_data"]["entries"]
+
+ # Convert back to JSON string
+ formatted_output = json.dumps(formatted_output, indent=4)
+
+
result = formatted_output
DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+
+ if not is_encrypted and not all(addr in addresses_to_exclude for addr in all_decrypted_addresses) and not "balance" in sys.argv and not "send" in sys.argv:
+ print("Wallet data does not appear to be encrypted. Returning un-encrypted data.\n")
+
return result
+#Transaction Functions
+def validate_and_select_node(node):
+ """
+ Overview:
+ This function is responsible for ensuring that the address of a Denaro node is valid and usable for
+ interactions with the blockchain network. It first checks if a node address is provided. If so, it
+ validates the address by calling the `validate_node_address` method. If no address is provided,
+ it defaults to a pre-defined, reliable node address. This function is essential for ensuring that
+ subsequent blockchain operations such as transactionsor balance queries are directed to a valid node.
+
+ Parameters:
+ node (str): The node address to validate. If None, a default node address is used.
+
+ Returns:
+ str or None: The function returns the node address if the validation is successful or the default
+ node address if no address is provided. It returns None if the provided address is invalid.
+ """
+ if node:
+ is_node_valid, node = Verification.validate_node_address(node)
+ if not is_node_valid:
+ node = 'https://denaro-node.gaetano.eu.org'
+ else:
+ node = 'https://denaro-node.gaetano.eu.org'
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not node])
+ return node
+
+def initialize_wallet(filename):
+ ensure_wallet_directories_exist()
+ filename = get_normalized_filepath(filename)
+ data, wallet_exists = _load_data(filename, False)
+
+ # Determine if wallet is encrypted
+ encrypted = False
+ if wallet_exists:
+ data_segment = json.dumps(data["wallet_data"])
+ encrypted = is_wallet_encrypted(data_segment)
+
+ result = wallet_exists, filename, encrypted
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+def checkBalance(filename, password, totp_code, address, node, to_json, to_file, show=None):
+ """
+ Checks the balance of cryptocurrency addresses.
+
+ :param filename: The wallet file name.
+ :param password: The password for the wallet file.
+ :param totp_code: The Time-based One-Time Password for additional security.
+ :param address: The cryptocurrency address or list of addresses to check.
+ :param node: The node to use for balance checking.
+ :param to_json: Flag to determine if output should be in JSON format.
+ :return: None or JSON data.
+ """
+ # Select a valid node
+ node = validate_and_select_node(node)
+ if node is None:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ encrypted = False
+ if filename:
+ wallet_exists, filename, encrypted = initialize_wallet(filename)
+ if not wallet_exists:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ # Error logging for encrypted wallet without a password
+ if encrypted and not password:
+ logging.error("Wallet is encrypted. A password is required.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ # Decrypt wallet entries
+ address_data = decryptWalletEntries(filename=filename, password=password, totp_code=totp_code if totp_code else "", address=address if address else [], fields=['address','id', "is_import"], pretty=False, show=show)
+ if not address_data:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+ entry_data = json.loads(address_data)['entry_data']
+ total_balance = 0
+ total_pending = 0
+ is_import = False
+
+ if show:
+ if "imported" in show:
+ if "imported_entries" in entry_data and "entries" in entry_data:
+ del entry_data["entries"]
+
+ if "generated" in show:
+ if "imported_entries" in entry_data:
+ del entry_data["imported_entries"]
+
+ if entry_data is not None:
+ if not to_json:
+ # Print balance information
+ print(f"Balance Information For: {filename}")
+ print("-----------------------------------------------------------")
+ for entry_feild in entry_data:
+ if entry_feild == "imported_entries":
+ is_import = True
+ for entry in entry_data[entry_feild]:
+ id = entry['id']
+ address = entry['address']
+ balance, pending_balance, is_error = get_balance_info(address, node)
+ if is_error:
+ break
+ total_balance += balance
+ total_pending += pending_balance
+ print(f'{"Imported " if is_import else ""}Address #{id}: {address}\nBalance: {balance} DNR{f" (Pending: {pending_balance} DNR)" if pending_balance != 0 else ""}\n')
+ print("\033[F-----------------------------------------------------------")
+ print(f'Total Balance: {total_balance} DNR')
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+
+ if to_json or to_file:
+ # Prepare JSON data
+ balance_data = {"balance_data": {"wallet_file_path": filename, "wallet_version":"0.2.2", "addresses": [], "imported_addresses" : [], "lastUpdated": datetime.utcnow().isoformat() + "Z"}}
+
+ if not "imported_entries" in entry_data or entry_data["imported_entries"] == []:
+ del balance_data["balance_data"]["imported_addresses"]
+
+ if not "entries" in entry_data or entry_data["entries"] == []:
+ del balance_data["balance_data"]["addresses"]
+
+ for entry_feild in entry_data:
+ if entry_feild == "imported_entries":
+ is_import = True
+ for entry in entry_data[entry_feild]:
+ address = entry['address']
+ balance, pending_balance, is_error = get_balance_info(address, node)
+ if is_error:
+ break
+ if not is_import:
+ balance_data["balance_data"]["addresses"].append({
+ "id": entry['id'],
+ "address": address,
+ "balance": {
+ "currency" : "DNR",
+ "amount" : str(balance)
+ }
+ })
+ else:
+ balance_data["balance_data"]["imported_addresses"].append({
+ "id": entry['id'],
+ "address": address,
+ "balance": {
+ "currency" : "DNR",
+ "amount" : str(balance)
+ }
+ })
+ if to_json:
+ print(json.dumps(balance_data, indent=4))
+ if to_file:
+ # Define the file path and name
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
+ wallet_name = os.path.splitext(os.path.basename(filename))[0]
+ balance_info_dir = os.path.join(os.path.dirname(filename), "balance_information")
+ file_directory = os.path.join(balance_info_dir, wallet_name)
+ file_name = f"{wallet_name}_balance_{timestamp}.json"
+ file_path = os.path.join(file_directory, file_name)
+
+ # Ensure balance_information directory exists
+ if not os.path.exists(balance_info_dir):
+ os.makedirs(balance_info_dir)
+
+ # Create wallet-specific directory if it doesn't exist
+ if not os.path.exists(file_directory):
+ os.makedirs(file_directory)
+
+ # Save the balance data to file
+ with open(file_path, 'w') as file:
+ json.dump(balance_data, file, indent=4)
+
+ print(f"\nBalance information saved to file: {file_path}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ else:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+def prepareTransaction(filename, password, totp_code, amount, sender, private_key, receiver, message, node):
+
+ node = validate_and_select_node(node)
+ if node is None:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ encrypted = False
+ address_pattern = r'^[DE][1-9A-HJ-NP-Za-km-z]{44}$'
+ if filename and sender and not private_key:
+
+ #Validate wallet address using regex pattern
+ if not re.match(address_pattern, sender):
+ logging.error("The wallet address provided is not valid.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ wallet_exists, filename, encrypted = initialize_wallet(filename)
+ if not wallet_exists:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ # If wallet is encrypted and password is not provided, log an error
+ if encrypted and not password:
+ logging.error("Wallet is encrypted. A password is required.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ decrypted_data = decryptWalletEntries(filename=filename, password=password, totp_code=totp_code if totp_code else "", address=[sender], fields=['private_key'], pretty=False)
+ if not decrypted_data is None:
+ decrypted_data = json.loads(decrypted_data)
+ private_key = decrypted_data['entry_data']['entries'][0]['private_key']
+
+ if private_key is None:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ if not filename and private_key:
+ # Validate private key using regex pattern
+ private_key_pattern = r'^(0x)?[0-9a-fA-F]{64}$'
+ if not re.match(private_key_pattern, private_key):
+ logging.error("The private key provided is not valid.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+ # Remove 0x prefix from private key if it exists
+ if private_key.startswith('0x'):
+ private_key = private_key[2:]
+ # Generate sending address from private key
+ generated_address = generate_from_private_key(private_key_hex=private_key, fields=["address"])
+ sender = generated_address['address']
+
+ # Convert private key to int
+ private_key = int(private_key, 16)
+
+ # Handle message variable
+ if message is None:
+ message = None
+ try:
+ message = bytes.fromhex(message)
+ except ValueError:
+ message = message.encode('utf-8')
+
+ #Validate receiving address using regex pattern
+ if not re.match(address_pattern, receiver):
+ logging.error("The recieving address is not valid.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ print(f"Attempting to send {amount} DNR from {sender} to {receiver}.\n")
+ # Create the transaction
+ result = create_transaction([private_key], sender, receiver, amount, message, node=node)
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
+
+def create_transaction(private_key, sender, receiving_address, amount, message: bytes = None, send_back_address=None, node=None):
+ amount = Decimal(amount)
+ inputs = []
+
+ for key in private_key:
+ if send_back_address is None:
+ send_back_address = sender
+ balance, address_inputs, is_pending, pending_transactions, is_error = get_address_info(sender, node)
+ if is_error:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+ for address_input in address_inputs:
+ address_input.private_key = key
+ inputs.extend(address_inputs)
+ if sum(input.amount for input in sorted(inputs, key=lambda item: item.amount, reverse=False)[:255]) >= amount:
+ break
+
+ if not inputs:
+ if is_pending:
+ logging.error("No spendable outputs. Please wait for pending transactions to be confirmed.")
+ if pending_transactions is not None:
+ print("\nTransactions awaiting confirmation:")
+ count = 0
+ for tx in pending_transactions:
+ count += 1
+ print(f"{count}: {tx[0]}")
+ else:
+ logging.error('No spendable outputs.')
+ if not balance > 0:
+ print("The associated address dose not have enough funds.")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ # Check if accumulated inputs are sufficient
+ if sum(input.amount for input in inputs) < amount:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ print("The associated address dose not have enough funds.")
+ return None
+
+ # Select appropriate transaction inputs
+ transaction_inputs = []
+ for tx_input in sorted(inputs, key=lambda item: item.amount, reverse=False):
+ transaction_inputs.append(tx_input)
+ if sum(input.amount for input in transaction_inputs) >= amount:
+ break
+
+ # Ensure that the transaction amount is adequate
+ transaction_amount = sum(input.amount for input in transaction_inputs)
+ if transaction_amount < amount:
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not transaction_amount])
+ logging.error(f"Consolidate outputs: send {transaction_amount} Denari to yourself")
+ return None
+
+ # Create the transaction
+ transaction = Transaction(transaction_inputs, [TransactionOutput(receiving_address, amount=amount)], message)
+ if transaction_amount > amount:
+ transaction.outputs.append(TransactionOutput(send_back_address, transaction_amount - amount))
+
+ # Sign and send the transaction
+ transaction.sign([private_key])
+
+ # Push transaction to node
+ try:
+ request = requests.get(f'{node}/push_tx', {'tx_hex': transaction.hex()}, timeout=10)
+ request.raise_for_status()
+ response = request.json()
+
+ if not response.get('ok'):
+ logging.error(response.get('error'))
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not transaction])
+ return transaction
+
+ except requests.RequestException as e:
+ # Handles exceptions that occur during the request
+ logging.error(f"Error during request to node: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ except ValueError as e:
+ # Handles JSON decoding errors
+ logging.error(f"Error decoding JSON response: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+ except KeyError as e:
+ # Handles missing keys in response data
+ logging.error(f"Missing expected data in response: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None
+
+def get_address_info(address: str, node: str):
+ try:
+ # Send the request to the node
+ request = requests.get(f'{node}/get_address_info', {'address': address, 'transactions_count_limit': 0, 'show_pending': True})
+ request.raise_for_status()
+
+ response = request.json()
+
+ if not response.get('ok'):
+ logging.error(response.get('error'))
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, None, None, True
+
+ result = response['result']
+ is_pending = False
+ tx_inputs = []
+ pending_spent_outputs = []
+
+ for value in result['pending_spent_outputs']:
+ pending_spent_outputs.append((value['tx_hash'], value['index']))
+
+ for spendable_tx_input in result['spendable_outputs']:
+ if (spendable_tx_input['tx_hash'], spendable_tx_input['index']) in pending_spent_outputs:
+ is_pending = True
+ continue
+
+ tx_input = TransactionInput(spendable_tx_input['tx_hash'], spendable_tx_input['index'])
+ tx_input.amount = Decimal(str(spendable_tx_input['amount']))
+ tx_input.public_key = string_to_point(address)
+ tx_inputs.append(tx_input)
+
+ final_result = Decimal(result['balance']), tx_inputs, is_pending, pending_spent_outputs, False
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not final_result])
+ return final_result
+
+ except requests.RequestException as e:
+ # Handles exceptions that occur during the request
+ logging.error(f"Error during request to node: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, None, None, True
+
+ except ValueError as e:
+ # Handles JSON decoding errors
+ logging.error(f"Error decoding JSON response: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, None, None, True
+
+ except KeyError as e:
+ # Handles missing keys in response data
+ logging.error(f"Missing expected data in response: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, None, None, True
+
+def get_balance_info(address: str, node: str):
+ """
+ Fetches the account data from the node and calculates the pending balance.
+
+ :param address: The address of the account.
+ :param node: The node URL to fetch data from.
+ :return: The total balance and pending balance of the account.
+ :raises: ConnectionError, ValueError, KeyError
+ """
+ try:
+ # Send the request to the node
+ request = requests.get(f'{node}/get_address_info', params={'address': address, 'show_pending': True})
+ request.raise_for_status() # Raises an HTTPError if the HTTP request returned an unsuccessful status code
+
+ response = request.json()
+ result = response.get('result')
+
+ if not response.get('ok'):
+ logging.error(response.get('error'))
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, True
+
+ # Handle potential missing 'result' key
+ if result is None:
+ logging.error("Missing 'result' key in response")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, True
+
+ pending_transactions = result.get('pending_transactions', [])
+ spendable_outputs = result.get('spendable_outputs', [])
+
+ # Create a set of spendable transaction hashes for easy lookup
+ spendable_hashes = {output['tx_hash'] for output in spendable_outputs}
+
+ # Ensure the balance is a string before converting to Decimal
+ total_balance = Decimal(str(result['balance']))
+ pending_balance = Decimal('0')
+
+ for transaction in pending_transactions:
+ # Adjust the balance based on inputs
+ for input in transaction.get('inputs', []):
+ if input.get('address') == address and input.get('tx_hash') in spendable_hashes:
+ input_amount = Decimal(str(input.get('amount', '0')))
+ pending_balance -= input_amount
+
+ # Adjust the balance based on outputs
+ for output in transaction.get('outputs', []):
+ if output.get('address') == address:
+ output_amount = Decimal(str(output.get('amount', '0')))
+ pending_balance += output_amount
+
+ # Format the total balance and pending balance to remove unnecessary trailing zeros
+ formatted_total_balance = total_balance.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
+ formatted_pending_balance = pending_balance.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
+
+ balance_data = formatted_total_balance, formatted_pending_balance, False
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not balance_data])
+ return balance_data
+
+ except requests.RequestException as e:
+ # Handles exceptions that occur during the request
+ logging.error(f"Error during request to node: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, True
+
+ except ValueError as e:
+ # Handles JSON decoding errors
+ logging.error(f"Error decoding JSON response: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, True
+
+ except KeyError as e:
+ # Handles missing keys in response data
+ logging.error(f"Missing expected data in response: {e}")
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None])
+ return None, None, True
+
# Argparse Helper Functions
def sort_arguments_based_on_input(argument_names):
- """Overview:
+ """
+ Overview:
Sorts a list of CLI argument names based on their positional occurrence in sys.argv.
Any argument not found in sys.argv is filtered out. The returned list is then formatted
- as a comma-separated string.
+ as a comma-separated string. This version also handles arguments with an '=' sign.
Parameters:
- argument_names (list): A list of argument names to be sorted.
@@ -866,21 +1593,26 @@ def sort_arguments_based_on_input(argument_names):
Note:
This function leverages the sys.argv array, which captures the command-line arguments passed to the script.
"""
- # Filter out arguments that are not present in sys.argv
- filtered_args = [arg for arg in argument_names if arg in sys.argv]
- # Sort the filtered arguments based on their index in sys.argv
- sorted_args = sorted(filtered_args, key=lambda x: sys.argv.index(x))
- # If there are multiple arguments, join them into a string separated by commas, adding 'and' before the last argument
+ # Process each argument in sys.argv to extract the argument name before the '=' sign
+ processed_argv = [arg.split('=')[0] for arg in sys.argv]
+
+ # Filter out arguments that are not present in the processed sys.argv
+ filtered_args = [arg for arg in argument_names if arg in processed_argv]
+
+ # Sort the filtered arguments based on their index in the processed sys.argv
+ sorted_args = sorted(filtered_args, key=lambda x: processed_argv.index(x))
+
+ # Join the arguments into a string with proper formatting
if len(sorted_args) > 1:
- return ', '.join(sorted_args[:-1]) + ', and ' + sorted_args[-1]
- # If there is only one argument, return it as a standalone string
+ result = ', '.join(sorted_args[:-1]) + ', and ' + sorted_args[-1]
elif sorted_args:
- return sorted_args[0]
- # If no arguments are present in sys.argv, return an empty string
+ result = sorted_args[0]
else:
- return ''
+ result = ''
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not result])
+ return result
-def check_args(parser,args):
+def check_args(parser, args):
"""Overview:
Validates combinations of CLI arguments and returns an error message via the parser
if invalid combinations are found. Specifically, it checks for required combinations
@@ -894,44 +1626,72 @@ def check_args(parser,args):
Utilizes the `sort_arguments_based_on_input` function to display arguments in the
order in which they were passed in the command line.
"""
- # -deterministic, -2fa, and -encrypt requires -password
- if args.deterministic and args.tfa and args.encrypt and not args.password:
- sorted_args = sort_arguments_based_on_input(['-deterministic', '-2fa', '-encrypt', '-password'])
- parser.error(f"\n{sorted_args} requires the -password argument to be set.\nContext: A password is required to encrypt the wallet, enable 2-Factor Authentication, and for deterministic address generation.")
-
- # -2fa and -encrypt requires -password
- if args.tfa and args.encrypt and not args.password:
- sorted_args = sort_arguments_based_on_input(['-2fa', '-encrypt', '-password'])
- parser.error(f"\n{sorted_args} requires the -password argument to be set.\nContext: A password is required for encrypted wallets with 2-Factor Authentication enabled.")
-
- # -2fa requires both -encrypt and -password
- if args.tfa and (not args.encrypt or not args.password):
- sorted_args = sort_arguments_based_on_input(['-2fa', '-encrypt', '-password'])
- if not args.encrypt:
- context_str = "2-Factor Authentication is only supported for encrypted wallets."
- if not args.password:
- context_str = "2-Factor Authentication is only supported for encrypted wallets, which requires a password."
- # -2fa and -deterministic requires both -encrypt and -password
- if args.deterministic:
- sorted_args = sort_arguments_based_on_input(['-2fa', '-deterministic', '-encrypt', '-password'])
+ if args.command == "generatewallet":
+ # -deterministic, -2fa, and -encrypt requires -password
+ if args.deterministic and args.tfa and args.encrypt and not args.password:
+ sorted_args = sort_arguments_based_on_input(['-deterministic', '-2fa', '-encrypt', '-password'])
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args])
+ parser.error(f"{sorted_args} requires the -password argument to be set.\nContext: A password is required to encrypt the wallet, enable 2-Factor Authentication, and for deterministic address generation.")
+
+ # -2fa and -encrypt requires -password
+ if args.tfa and args.encrypt and not args.password:
+ sorted_args = sort_arguments_based_on_input(['-2fa', '-encrypt', '-password'])
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args])
+ parser.error(f"{sorted_args} requires the -password argument to be set.\nContext: A password is required for encrypted wallets with 2-Factor Authentication enabled.")
+
+ # -2fa requires both -encrypt and -password
+ if args.tfa and (not args.encrypt or not args.password):
+ sorted_args = sort_arguments_based_on_input(['-2fa', '-encrypt', '-password'])
+ if not args.encrypt:
+ context_str = "2-Factor Authentication is only supported for encrypted wallets."
+
if not args.password:
- context_str += " Deterministic address generation also requires a password."
- parser.error(f"\n{sorted_args} requires both the -encrypt and -password arguments to be set.\nContext: {context_str}")
-
- # -encrypt and -deterministic requires -password
- if args.encrypt and args.deterministic and not args.password:
- sorted_args = sort_arguments_based_on_input(['-encrypt', '-deterministic', '-password'])
- parser.error(f"\n{sorted_args} requires the -password argument to be set.\nContext: A password is required to encrypt the wallet and for deterministic address generation.")
-
- # -deterministic alone requires -password
- if args.deterministic and not args.password:
- sorted_args = sort_arguments_based_on_input(['-deterministic', '-password'])
- parser.error(f"\n{sorted_args} requires the -password argument to be set.\nContext: A password is required for deterministic address generation.")
-
- # -encrypt alone requires -password
- if args.encrypt and not args.password:
- sorted_args = sort_arguments_based_on_input(['-encrypt', '-password'])
- parser.error(f"\n{sorted_args} requires the -password argument to be set.\nContext: A password is required to encrypt the wallet.")
+ context_str = "2-Factor Authentication is only supported for encrypted wallets, which requires a password."
+
+ # -2fa and -deterministic requires both -encrypt and -password
+ if args.deterministic:
+ sorted_args = sort_arguments_based_on_input(['-2fa', '-deterministic', '-encrypt', '-password'])
+ if not args.password:
+ context_str += " Deterministic address generation also requires a password."
+
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args and var is not context_str])
+ parser.error(f"{sorted_args} requires both the -encrypt and -password arguments to be set.\nContext: {context_str}")
+
+ # -encrypt and -deterministic requires -password
+ if args.encrypt and args.deterministic and not args.password:
+ sorted_args = sort_arguments_based_on_input(['-encrypt', '-deterministic', '-password'])
+ parser.error(f"{sorted_args} requires the -password argument to be set.\nContext: A password is required to encrypt the wallet and for deterministic address generation.")
+
+ # -deterministic alone requires -password
+ if args.deterministic and not args.password:
+ sorted_args = sort_arguments_based_on_input(['-deterministic', '-password'])
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args])
+ parser.error(f"{sorted_args} requires the -password argument to be set.\nContext: A password is required for deterministic address generation.")
+
+ # -encrypt alone requires -password
+ if args.encrypt and not args.password:
+ sorted_args = sort_arguments_based_on_input(['-encrypt', '-password'])
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args])
+ parser.error(f"{sorted_args} requires the -password argument to be set.\nContext: A password is required to encrypt the wallet.")
+
+ if args.command == "send":
+ # -wallet and -private-key cannot be used together
+ if args.wallet and args.private_key:
+ sorted_args = sort_arguments_based_on_input(['-wallet', '-private-key'])
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args])
+ parser.error(f"{sorted_args} cannot be used together.\nContext: The script automatically retrieves the private key of the specified address from the wallet file. The -private-key arguemnt is unnessesary in this instance.")
+
+ # -wallet requires -address
+ if args.wallet and not args.sender:
+ sorted_args = sort_arguments_based_on_input(['-wallet', '-address'])
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args])
+ parser.error(f"{sorted_args} requires the -address argument to be set.\nContext: An address that is associated with the wallet file must be specified.")
+
+ # -address requires -wallet
+ if args.sender and not args.wallet:
+ sorted_args = sort_arguments_based_on_input(['-address', '-wallet'])
+ DataManipulation.secure_delete([var for var in locals().values() if var is not None and var is not sorted_args])
+ parser.error(f"{sorted_args} requires the -wallet argument to be set.\nContext: A wallet file must be specified in order to use the given address. The address should also be associated with the wallet file.")
def process_decryptwallet_filter(args):
"""Overview:
@@ -960,86 +1720,102 @@ def process_decryptwallet_filter(args):
- tuple: A tuple consisting of the filtered address, the filtered field(s), and the value of args.filter_subparser_pretty.
"""
# Initialize address and field variables
- address = []
+ addresses = []
field = []
fields_to_string = ""
-
- if args.filter:
- # Check if the filter argument is enclosed in quotes
- # This is necessary to ensure that the argument is parsed correctly
- filter_args = [item.replace("-filter=", "") for item in sys.argv[1:] if "-filter" in item]
- using_quotes = len(filter_args) == 1
- # If quotes are not used, raise an error
- if not using_quotes:
- raise argparse.ArgumentTypeError("Invalid filter syntax. The -filter argument must be enclosed in quotes.")
-
- # Manually parse the args.filter string into a dictionary to check key spelling
- parsed_filter_keys = []
- for part in args.filter.split(','):
- if '=' in part:
- key, _ = part.split('=')
- parsed_filter_keys.append(key)
-
- # List of valid keys for filtering
- valid_keys = ["address", "field"]
-
- # Raise an error if an invalid key is found
- for key in parsed_filter_keys:
- if key not in valid_keys:
- raise argparse.ArgumentTypeError(f"Invalid filter key: '{key}'. Valid keys are {valid_keys}.")
+ filter_subparser_pretty = False
+ filter_subparser_show = None
+ if not args.command == 'balance':
+ if args.filter:
+ # Check if the filter argument is enclosed in quotes
+ # This is necessary to ensure that the argument is parsed correctly
+ filter_args = [item.replace("-filter=", "") for item in sys.argv[1:] if "-filter" in item]
+ using_quotes = len(filter_args) == 1
+ # If quotes are not used, raise an error
+ if not using_quotes:
+ raise argparse.ArgumentTypeError("Invalid filter syntax. The -filter argument must be enclosed in quotes.")
+
+ # Manually parse the args.filter string into a dictionary to check key spelling
+ parsed_filter_keys = []
+ for part in args.filter.split(','):
+ if '=' in part:
+ key, _ = part.split('=')
+ parsed_filter_keys.append(key)
+
+ # List of valid keys for filtering
+ valid_keys = ["address", "field"]
+
+ # Raise an error if an invalid key is found
+ for key in parsed_filter_keys:
+ if key not in valid_keys:
+ raise argparse.ArgumentTypeError(f"Invalid filter key: '{key}'. Valid keys are {valid_keys}.")
+
+ # Validate the syntax of the filter argument
+ filter_str = args.filter
+ valid, error = validate_filter_string(filter_str)
+ if not valid:
+ raise argparse.ArgumentTypeError(error)
+
+ # Extract the values for 'address' and 'field' from the filter argument
+ address_match = re.search(r'address=\{(.+?)\}', filter_str)
+ field_match = re.search(r'field=\{(.+?)\}', filter_str)
+
+ # Assign values to 'address' and 'field' variables
+ if address_match:
+ addresses = address_match.group(1).split(',')
+ if field_match:
+ field = field_match.group(1).split(',')
+ fields_to_string = ", ".join(field)
- # Validate the syntax of the filter argument
- filter_str = args.filter
- valid, error = validate_filter_string(filter_str)
- if not valid:
- raise argparse.ArgumentTypeError(error)
-
- # Extract the values for 'address' and 'field' from the filter argument
- address_match = re.search(r'address=\{(.+?)\}', filter_str)
- field_match = re.search(r'field=\{(.+?)\}', filter_str)
-
- # Assign values to 'address' and 'field' variables
- if address_match:
- address = address_match.group(1).split(',')
- if field_match:
- field = field_match.group(1).split(',')
- fields_to_string = ", ".join(field)
-
- # Handle the case when the 'filter' subparser is used
- if args.filter_subparser == 'filter':
+ # Handle the case when the 'filter' subparser is used
+ if args.filter_subparser == 'filter':
+ filter_subparser_pretty = args.filter_subparser_pretty
+ filter_subparser_show = args.filter_subparser_show
+ if args.address:
+ addresses = args.address.split(',')
+ if args.field:
+ field = args.field.split(',')
+ fields_to_string = ", ".join(field)
+
+ # If no subparser is used, set pretty printing to False
+ elif args.filter_subparser != 'filter':
+ args.filter_subparser_pretty = False
+ args.filter_subparser_show = None
+ # Validate the field values against a list of valid options
+ valid_fields = ["id","mnemonic", "private_key", "public_key", "address"]
+ if field:
+ for f in field:
+ if f not in valid_fields:
+ raise ValueError(f"Invalid field value: {f}. Must be one of {valid_fields}")
+ else:
if args.address:
- address = args.address.split(',')
- if args.field:
- field = args.field.split(',')
- fields_to_string = ", ".join(field)
-
- # If no subparser is used, set pretty printing to False
- elif args.filter_subparser != 'filter':
- args.filter_subparser_pretty = False
-
- # Validate the field values against a list of valid options
- valid_fields = ["id","mnemonic", "private_key", "public_key", "address"]
- if field:
- for f in field:
- if f not in valid_fields:
- raise ValueError(f"Invalid field value: {f}. Must be one of {valid_fields}")
+ addresses = args.address.split(',')
#Remove duplicate addresses
seen_addresses = set()
- address = [entry for entry in address if entry not in seen_addresses and not seen_addresses.add(entry)]
- address = remove_duplicates_from_address_filter(address)
+ addresses = [entry for entry in addresses if entry not in seen_addresses and not seen_addresses.add(entry)]
+ addresses = remove_duplicates_from_address_filter(addresses)
+
+ #Validate addresses using regex pattern
+ address_pattern = r'^-?[DE][1-9A-HJ-NP-Za-km-z]{44}$'
+ valid_addresses = [addr for addr in addresses if re.match(address_pattern, addr)]
+ invalid_addresses = [addr for addr in addresses if addr not in valid_addresses]
+ if len(invalid_addresses) >= 1:
+ print(f"Warning: The following {'address is' if len(invalid_addresses) == 1 else 'addresses are'} not valid: {invalid_addresses}")
+ if not len(valid_addresses) >=1:
+ print()
+ addresses = valid_addresses
# Output the filtering criteria to the console
- if address and not field:
- print(f'\nFiltering wallet by address: "{address}"')
- if not address and field:
- print(f'\nFiltering entries by field: "{fields_to_string}"')
- if address and field:
- print(f'\nFiltering wallet by address: "{address}"')
- print(f'Filtering address entry by field: "{fields_to_string}"')
-
+ if addresses and not field:
+ print(f'Filtering wallet by address: "{addresses}"\n')
+ if not addresses and field:
+ print(f'Filtering entries by field: "{fields_to_string}"\n')
+ if addresses and field:
+ print(f'Filtering wallet by address: "{addresses}"\n')
+ print(f'Filtering address entry by field: "{fields_to_string}"\n')
# Return the filtering criteria and pretty printing option
- return address, field, args.filter_subparser_pretty
+ return addresses, field, filter_subparser_pretty, filter_subparser_show
def validate_filter_string(input_string):
"""Overview:
@@ -1141,15 +1917,18 @@ def main():
# Verbose parser for shared arguments
verbose_parser = argparse.ArgumentParser(add_help=False)
verbose_parser.add_argument('-verbose', action='store_true', help='Enables info and debug messages.')
+
+ #Node URL parser
+ denaro_node = argparse.ArgumentParser(add_help=False)
+ denaro_node.add_argument('-node', type=str, help="Specifies the URL or IP address of a Denaro node.")
# Create the parser
- parser = argparse.ArgumentParser(description="Manage and decrypt wallet data.")
+ parser = argparse.ArgumentParser(description="Manage wallet data.")
subparsers = parser.add_subparsers(dest='command')
# Subparser for generating a new wallet
parser_generatewallet = subparsers.add_parser('generatewallet', parents=[verbose_parser])
-
- parser_generatewallet.add_argument('-wallet', required=True, help="Specify the wallet filename.")
+ parser_generatewallet.add_argument('-wallet', required=True, help="Specifies the wallet filename. A filepath can be specified before the filename, if not then the default './wallets/' filepath will be used.")
parser_generatewallet.add_argument('-encrypt', action='store_true', help="Encrypt the new wallet.")
parser_generatewallet.add_argument('-2fa', dest='tfa', action='store_true', help="Enables 2FA for a new encrypted wallet.")
parser_generatewallet.add_argument('-password', help="Password used for wallet encryption and/or deterministic address generation.")
@@ -1160,15 +1939,16 @@ def main():
# Subparser for generating a new address
parser_generateaddress = subparsers.add_parser('generateaddress', parents=[verbose_parser])
- parser_generateaddress.add_argument('-wallet', required=True, help="Specify the wallet filename.")
+ parser_generateaddress.add_argument('-wallet', required=True, help="Specifies the wallet filename. A filepath can be specified before the filename, if not then the default './wallets/' filepath will be used.")
parser_generateaddress.add_argument('-2fa-code', dest='tfacode', type=str, required=False, help="Two-Factor Authentication code for 2FA enabled wallets.")
parser_generateaddress.add_argument('-password', help="The password used for encryption and/or deterministic address generation of the specified wallet file.")
-
+ parser_generateaddress.add_argument('-amount', type=int, help="Specifies the amount of addresses to generate.")
+
# Subparser for decrypting the wallet
parser_decryptwallet = subparsers.add_parser('decryptwallet', parents=[verbose_parser])
- parser_decryptwallet.add_argument('-wallet', required=True, help="Specify the wallet filename.")
+ parser_decryptwallet.add_argument('-wallet', required=True, help="Specifies the wallet filename. A filepath can be specified before the filename, if not then the default './wallets/' filepath will be used.")
parser_decryptwallet.add_argument('-2fa-code', dest='tfacode', type=str, required=False, help="Two-Factor Authentication code for 2FA enabled wallets.")
- parser_decryptwallet.add_argument('-pretty', action='store_true', help="Print formatted json output for enhanced readability.")
+ parser_decryptwallet.add_argument('-pretty', action='store_true', help="Prints formatted JSON output for enhanced readability.")
parser_decryptwallet.add_argument('-password', help="The password used for encryption of the specified wallet.")
parser_decryptwallet.add_argument('-filter', help='Filter entries by address and/or field. Add a hyphen (-) to the beginning of an address to exclude it. Format is: -filter="address={ADDRESS_1, ADDRESS_2, ADDRESS_3, ...},field={id,mnemonic,private_key,public_key,address}". The entire filter string must be enclosed in quotation marks and parameters must be enclosed in curly braces ("\u007B\u007D").', default=None)
@@ -1177,25 +1957,78 @@ def main():
parser_filter = filter_subparser.add_parser('filter', parents=[verbose_parser], help="Filter entries by address and/or field")
parser_filter.add_argument('-address', help='One or more addresses to filter by. Add a hyphen (-) to the beginning of an address to exclude it. Format is: `address=ADDRESS_1, ADDRESS_2, ADDRESS_3,...`')
parser_filter.add_argument('-field', help='One or more fields to filter by. Format is: `field=id,mnemonic,private_key,public_key,address`.')
- parser_filter.add_argument('-pretty', action='store_true', help="Print formatted json output for enhanced readability.", dest="filter_subparser_pretty")
+ parser_filter.add_argument('-show', choices=['generated', 'imported'], dest="filter_subparser_show", help="Filters information based on entry origin. Use 'generated' to retrieve only the information of wallet entries that have been internally generated. Use 'imported' to retrieve only the information of wallet entries that have been imported.")
+ parser_filter.add_argument('-pretty', action='store_true', dest="filter_subparser_pretty", help="Prints formatted JSON output for enhanced readability.")
+
+ # Subparser for importing wallet data based on a private key
+ parser_import = subparsers.add_parser('import', parents=[verbose_parser])
+ parser_import.add_argument('-wallet', required=True, help="Specifies the wallet filename. A filepath can be specified before the filename, if not then the default './wallets/' filepath will be used.")
+ parser_import.add_argument('-password', help="The password used for encryption of the specified wallet.")
+ parser_import.add_argument('-2fa-code', dest='tfacode', type=str, required=False, help="Two-Factor Authentication code for 2FA enabled wallets.")
+ parser_import.add_argument('-private-key', dest='private_key', required=True, help="Specifies the private key to import.")
+ # Subparser for sending a transaction
+ parser_send = subparsers.add_parser('send', parents=[verbose_parser, denaro_node])
+ parser_send.add_argument('-amount', required=True, help="The amount of Denaro to send.")
+
+ # Subparser to specify the wallet file and address to send from. The private key of an address can also be specified.
+ send_from_subparser = parser_send.add_subparsers(dest='transaction_send_from_subparser', required=True)
+ parser_send_from = send_from_subparser.add_parser('from', parents=[verbose_parser, denaro_node])
+ parser_send_from.add_argument('-wallet', help="Specifies the wallet filename. A filepath can be specified before the filename, if not then the default './wallets/' filepath will be used.")
+ parser_send_from.add_argument('-password', help="The password used for encryption of the specified wallet.")
+ parser_send_from.add_argument('-2fa-code', dest='tfacode', type=str, required=False, help="Two-Factor Authentication code for 2FA enabled wallets.")
+ parser_send_from.add_argument('-address', dest='sender', help="Wallet address to send from.")
+ parser_send_from.add_argument('-private-key', dest='private_key', help="Specifies the private key associated with the address to send from. Not required if using a wallet file.")
+
+ # Subparser to specify the receiving address and optional transaction message.
+ parser_send_to_subparser = parser_send_from.add_subparsers(dest='transaction_send_to_subparser', required=True)
+ parser_send_to = parser_send_to_subparser.add_parser('to', parents=[verbose_parser, denaro_node])
+ parser_send_to.add_argument('receiver', help="The receiveing address.")
+ parser_send_to.add_argument('-message', default="", help="Optional transaction message.")
+
+ # Subparser for checking balance
+ parser_balance = subparsers.add_parser('balance', parents=[verbose_parser, denaro_node])
+ parser_balance.add_argument('-wallet', required=True, help="Specifies the wallet filename. A filepath can be specified before the filename, if not then the default './wallets/' filepath will be used.")
+ parser_balance.add_argument('-password', help="The password used for encryption of the specified wallet.")
+ parser_balance.add_argument('-2fa-code', dest='tfacode', type=str, required=False, help="Two-Factor Authentication code for 2FA enabled wallets.")
+ parser_balance.add_argument('-address', help="Specifies the address to get the balance of.")
+ parser_balance.add_argument('-json', action='store_true', help="Prints the output of the blance information in JSON format.")
+ parser_balance.add_argument('-to-file', dest='to_file', action='store_true', help="Saves the output of the balance information to a file. The resulting file will be in JSON format and named as '[WalletName]_balance_[Timestamp].json' and stored in '/[WalletDirectory]/balance_information/[WalletName]/'. Example for './wallets/PersonalWallet.json': './wallets/balance_information/PersonalWallet/PersonalWallet_balance_2023-12-20_17-09-26.json'.")
+ parser_balance.add_argument('-show', choices=['generated', 'imported'], help="Filters balance information based on entry origin. Use 'generated' to retrieve only the balance information of wallet entries that have been internally generated. Use 'imported' to retrieve only the balance information of wallet entries that have been imported.")
args = parser.parse_args()
+
+
if args.command == "generatewallet":
- check_args(parser,args)
- address = generateAddressHelper(filename=args.wallet, password=args.password, totp_code=None, new_wallet=True, encrypt=args.encrypt, use2FA=args.tfa,deterministic=args.deterministic,backup=args.backup,disable_warning=args.disable_overwrite_warning,overwrite_password=args.overwrite_password)
+ check_args(parser, args)
+ address = generateAddressHelper(filename=args.wallet, password=args.password, totp_code=None, new_wallet=True, encrypt=args.encrypt, use2FA=args.tfa, deterministic=args.deterministic, backup=args.backup, disable_warning=args.disable_overwrite_warning, overwrite_password=args.overwrite_password)
if address:
- print(f"\nSuccessfully generated new wallet. Address: {address}")
+ print(address)
elif args.command == "generateaddress":
- address = generateAddressHelper(filename=args.wallet, password=args.password, totp_code=args.tfacode if args.tfacode else None, new_wallet=False, encrypt=False, use2FA=False)
+ address = generateAddressHelper(filename=args.wallet, password=args.password, totp_code=args.tfacode if args.tfacode else None, new_wallet=False, encrypt=False, use2FA=False, amount=args.amount if args.amount else 1)
if address:
- print(f"\nSuccessfully generated address and stored wallet entry. Address: {address}")
+ print(address)
elif args.command == 'decryptwallet':
- address, field, args.filter_subparser_pretty = process_decryptwallet_filter(args)
- decrypted_data = decryptWalletEntries(filename=args.wallet, password=args.password, totp_code=args.tfacode if args.tfacode else "", address=address if address else None, fields=field if field else [], pretty=args.pretty or args.filter_subparser_pretty if args.pretty or args.filter_subparser_pretty else False)
+ address, field, args.filter_subparser_pretty, args.filter_subparser_show = process_decryptwallet_filter(args)
+ decrypted_data = decryptWalletEntries(filename=args.wallet, password=args.password, totp_code=args.tfacode if args.tfacode else "", address=address if address else None, fields=field if field else [], pretty=args.pretty or args.filter_subparser_pretty if args.pretty or args.filter_subparser_pretty else False, show=args.filter_subparser_show if args.filter_subparser_show else None)
if decrypted_data:
- print(f'\nWallet Data:\n"{decrypted_data}"')
+ print(f'Wallet Data:\n"{decrypted_data}"')
+
+ elif args.command == 'send':
+ check_args(parser, args)
+ transaction = prepareTransaction(filename=args.wallet, password=args.password, totp_code=args.tfacode if args.tfacode else "", amount=args.amount, sender=args.sender if args.sender else None, private_key=args.private_key if args.private_key else None, receiver=args.receiver, message=args.message, node=args.node)
+ if transaction:
+ print(f'Transaction successfully pushed to node. \nTransaction hash: {sha256(transaction.hex())}')
+ print(f'\nDenaro Explorer link: http://explorer.denaro.is/transaction/{sha256(transaction.hex())}')
+
+ elif args.command == 'balance':
+ address, _, _, _ = process_decryptwallet_filter(args)
+ checkBalance(filename=args.wallet, password=args.password, totp_code=args.tfacode if args.tfacode else "", address=address if args.address else None, node=args.node, to_json=args.json, to_file=args.to_file, show=args.show)
+
+ elif args.command == 'import':
+ generateAddressHelper(filename=args.wallet, password=args.password, totp_code=args.tfacode if args.tfacode else None, new_wallet=False, encrypt=False, use2FA=False, private_key=args.private_key, is_import=True)
+
DataManipulation.secure_delete([var for var in locals().values() if var is not None])
if __name__ == "__main__":
@@ -1208,10 +2041,10 @@ def main():
print("\rProcess terminated by user.")
QRCodeUtils.close_qr_window(True)
exit_code = 1
- except Exception as e:
- logging.error(f"{e}")
- exit_code = 1
+ #except Exception as e:
+ # logging.error(f"{e}")
+ # exit_code = 1
finally:
DataManipulation.secure_delete([var for var in locals().values() if var is not None])
gc.collect()
- #sys.exit(exit_code)
+ sys.exit(exit_code)
\ No newline at end of file