Skip to content

Commit

Permalink
Major Update to the Wallet Client
Browse files Browse the repository at this point in the history
This release brings a comprehensive overhaul to the wallet client and
introduces a wide range of new features such as transaction support,
balance checking, choosing a Denaro node, the ability to import wallet
entries, multi-address generation, Wallet Annihilation, and new CLI
options. This release also introduces numerous improvements and
additions to core features, organizational restructuring of the
internal code-base, and changes to the JSON data structure of wallet
files.

Changelog for Denaro Wallet Client v0.0.5-beta:

Organizational Changes:
- Restructured `cryptographic_util.py`:
  - Divided various classes into separate utility modules for
    enhanced optimization and organization.

  - Renamed 'TOTP_Utils' to 'TOTP'.

  - Renamed 'CryptoWallet' to 'EncryptDecryptUtils'.

  - Merged 'EncryptionUtils' class methods into
    'EncryptDecryptUtils'.

  - Renamed 'VerificationUtils' to 'Verification' and relocated
    to a new module `verification_utils.py`.

  - Moved 'DataManipulation' class to a new module
    `data_manipulation_util.py`.

  - Retained 'ProofOfWork', 'TOTP', and 'EncryptDecryptUtils'
    classes in `cryptographic_util.py`.

  - Renamed 'key_generation.py' to 'wallet_generation_util.py'.

  - Relocated all utility modules to `denaro/wallet/utils`
    directory.

Transaction Support:
- New modules have been added to facilitate transaction support.
  - These new modules include: `transaction.py`,
    `transaction_input.py`, `transaction_output.py`, and
    `coinbase_transaction.py`.

- Various functions have been added to `wallet_client.py` to
  facilitate transaction and balance management.
  - These new functions include: `prepareTransaction`,
    `create_transaction`, `get_address_info`, `checkBalance`,
    `get_balance_info`, `validate_and_select_node`, and
    `initialize_wallet`.

CLI changes:
 Note: This changelog dose not include full usage documentation.
 Documentation will be made availible at a later time to reflect
 these changes.

  - A `send` sub-command with multiple options has been added,
    allowing users to initiate transactions.

  - A `balance` sub-command with multiple options has been added,
    allowing users to check the balance of addresses associated
    with their wallet.

  - An `-import` sub-command has been added to facilitate the
    import of wallet entries based on the private key associated
    with an address.

  - Introduced a `-show` option to the `decryptwallet` and
    `balance` sub-commands, allowing users to selectively filter
    wallet entries based on their origin (See the 'Changes to
    Wallet JSON Data' section for more infornation).
    This option includes two choices:
    - `-show generated`: Retrieves only wallet entries that have
      been internally generated.

    - `-show imported`: Retrieves only wallet entries that have
      been imported by the user.

  - Introduced a `-node` option for the `send` and `balance`
    sub-commands, allowing users to choose a preferred Denaro
    node for conducting transactions or checking balances. This
    option requires a valid IP or URL string. If a node is not
    specified by the user, then the wallet client will use the
    default Denaro node (https://denaro-node.gaetano.eu.org)

  - Introduced an `-amount` option to the `generateaddress`
    sub-command, enabling users to generate multiple addresses in
    a single command, with a maximum limit of 256 addresses
    (subject to change).

Improvements and Additions:
  - Conducted a thorough refactoring of verbose logging mechanisms
    within the `generateAddressHelper` function for improved
    clarity and efficiency.

  - Enhanced the `generateAddressHelper` function to support
    multi-address generation and to streamline the process of
    importing wallet entries.

  - After wallet data has been generated, a warning about the
    risks of disclosing mnemonic phrases or private keys will be
    shown to the user.

  - After wallet data has been generated, the cryptographic keys
    along with the Denaro address associated with the data will be
    shown to the user. For non-deterministic wallet data, the
    mnemonic phrase, private key, and address are displayed, while
    for deterministic wallet data, only the private key and
    address are shown. The master mnemonic is also displayed but
    only when a new wallet is generated.

  - Added a `generate_from_private_key` function to
    `wallet_generation_util.py` for generating wallet data
    directly from a private key.

  - Refactored the `decryptWalletEntries` function to allow for
    both the output of unencrypted wallet data and the capability
    to filter and process wallet entries based on their origin
    (See the 'Changes to Wallet JSON Data' section and the new
    `-show` option for more information).

  - Incorporated various messages within the `decryptWalletEntries`
    function to accommodate the newly introduced transaction and
    balance checking features.

  - Integrated a feature in the wallet decryption process to
    display a count of decrypted entries out of the total number
    of entries (e.g. "Decrypting wallet entry 3 of 15").

  - Implemented validation of wallet addresses and private keys
    using regular expression patterns.

  - Modified the `sort_arguments_based_on_input` and `check_args`
    functions to ensure proper usage of CLI arguments and to
    prevent erroneous combinations that could lead to errors.

  - Addressed and corrected various typos throughout the codebase.

  - Slightly optimized some functions for enhanced efficiency and
    performance.

  - The Wallet Annihilation feature now only performs a single
    deletion pass instead of two, reducing operational overhead.

  - Eliminated repetitive code in the `update_failed_attempts` and
    `reset_failed_attempts` methods by consolidating common
    functionalities into a singular `get_failed_attempts` method,
    thereby reducing code redundancy.

  - Implemented robust validation using regular expressions for IP
    addresses, URLs, and port numbers. This specifically applies
    when utilizing the `-node` option. The validation process
    rigorously checks the format of the entered values, ensuring
    they conform to standard IP, URL, and port number patterns.
    This also ensures secure and error-free connections for
    subsequent web requests.

    - Node verification has also been implemented to check if the
      user-provided IP or URL might be associated with actual
      Denaro node (See the 'Denaro Node Verification' section for
      more information).

Changes to Wallet JSON Data Structure:
- The `imported_entries` object array is a new addition to the
  wallet architecture, and has been implemented to accomidate the
  new import feature. This array is used specifically for storing
  wallet entries that have been imported by the user, and will be
  added to the JSON structure of a wallet file whenever a user
  first performs an import. The `imported_entries` array is distinct
  from the `entries` array which holds internally generated wallet
  entries. The introduction of `imported_entries` serves several
  purposes:

  1. Data Segregation: It provides a clear seperatation
     between entries originating from what has been internally
     generated by the wallet client (stored in `entries`) and
     those imported by the user (stored in `imported_entries`).
     This segregation is vital for maintaining data organization
     and clarity within wallet JSON data.

  2. Deterministic Wallet Data Generation: The deterministic
     generation of wallet data relies on the total number of
     objects stored in the `entries` array. Specifically, this
     count (+1) is used as an index in the address derivation path
     for generating child keys and their corresponding addresses.
     Introducing data from external sources into the `entries`
     array would disrupt this deterministic process. The
     separation of `imported_entries` ensures the integrity and
     consistency of the deterministic generation of wallet data by
     isolating imported data from the internal generation process.

  3. Operational Integrity: By keeping imported entries in a
     separate array, the wallet system avoids potential conflicts
     and errors that could arise from mixing externally sourced
     data with internally generated data. This separation is
     fundamental to the reliable and secure operation of the
     wallet.

In summary, `imported_entries` enhances the wallet's functionality
by providing a dedicated space for user-imported entries, thereby
protecting the integrity of the deterministic generation process
and ensuring the wallet's overall operational efficiency and
security.

Denaro Node Verification:
  - A verification procedure has been introduced to confirm the
    authenticity of a Denaro node specified by the user. This
    specifically applies when utilizing the `-node` option. This
    process is critical for ensuring secure and accurate network
    connections. However, this process is not 100% foolproof (See
    'Potential Security Implication').

  - Initial Verification Step:
    - The process begins by retrieving the latest block number from a
      trusted Denaro node (e.g., `https://denaro-node.gaetano.eu.org/
      get_mining_info`). This block number serves as a reference
      point for the validation process.

  - Random Block Number Generation:
    - A random block number is generated, ranging between 0 and the
      latest block number retrieved. This step introduces a layer of
      unpredictability, enhancing the robustness of the verification
      process.

  - Block Hash Retrieval and Comparison:
    - The trusted Denaro node is queried to obtain the block hash
      corresponding to the randomly generated block number (e.g.
      `https://denaro-node.gaetano.eu.org/get_block?block=123456`).

    - A subsequent request is made to the user-provided IP or URL to
      retrieve the block hash for the same block number.

    - The hashes obtained from both the trusted and user-provided
      nodes are then compared.

  - Final Verification:
    - If the block hash from the user-provided node is the same as
      that from the trusted node, then it is a strong indicator that
      the user-provided node is a legitimate Denaro node. Assuming
      that it's blockchain is in-sync.

    - After the process is complete then all subsequent requests for
      transactions or balance checking will be made using the
      user-provided node.

    - If verification fails then the wallet client will use the
      default Denaro node (https://denaro-node.gaetano.eu.org).

  - Potential Security Implication – Proxy/Relay Nodes:
    - It's crucial to recognize that this method is not 100%
      foolproof. A server could function as a proxy or relay,
      forwarding requests to the trusted node. This behavior could
      mask the server's identity, making it appear as a legitimate
      Denaro node.

    - This means that even if the block hashes match, there is a
      possibility that the user-provided node might not be an actual
      Denaro node, but rather a malicious server relaying information
      from a genuine node. This can deceive users into believing they
      are interacting with a trusted node.

    - While the relayed data might be accurate, the intermediary
      server (proxy/relay) could potentially intercept, log, or even
      manipulate the data being transmitted between the user and the
      Denaro blockchain. This could lead to sensitive information
      being compromised, incorrect balance information, or even the
      execution of malicious transactions.

    - In light of the potential security implication it is imperative
      for users to protect their assets and security by being
      well-informed, vigilant, and to use trusted Denaro Nodes.
  • Loading branch information
The-Sycorax committed Dec 31, 2023
1 parent e347b66 commit b10a9af
Show file tree
Hide file tree
Showing 12 changed files with 2,806 additions and 1,303 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
<dl><dd>

Expand Down Expand Up @@ -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.
935 changes: 0 additions & 935 deletions denaro/wallet/cryptographic_util.py

This file was deleted.

450 changes: 450 additions & 0 deletions denaro/wallet/utils/cryptographic_util.py

Large diffs are not rendered by default.

423 changes: 423 additions & 0 deletions denaro/wallet/utils/data_manipulation_util.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand All @@ -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.")
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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])
data_manipulation_util.DataManipulation.secure_delete([var for var in locals().values() if var is not None])
50 changes: 50 additions & 0 deletions denaro/wallet/utils/transaction_utils/coinbase_transaction.py
Original file line number Diff line number Diff line change
@@ -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())
Loading

0 comments on commit b10a9af

Please sign in to comment.