Merge pull request #1558 from wojake/patch-11

Create airgapped-wallet.py
This commit is contained in:
Rome Reginelli
2022-11-23 12:19:38 -08:00
committed by GitHub
5 changed files with 645 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
# Airgapped Wallet
Airgapped describes a state where a device or a system becomes fully disconnected from other devices and systems. It is the maximum protection for a system against unwanted visitors/viruses, this allows any sensitive data like a private key to be stored without worry of it being compromised as long as reasonable security practices are being practiced.
This airgapped XRP wallet allows users to sign a Payment transaction in a secure environment without the private key being exposed to a machine connected to the internet. The private key and seed is encrypted by password and stored securely.
*Note*: You should not use this airgapped wallet in production, it should only be used for educational purposes only.
This code sample consists of 2 parts:
- `airgapped-wallet.py` - This code should be stored in a standalone airgapped machine, it consist of features to generate a wallet, store a keypair securely, sign a transaction and share the signed transaction via QR code.
- `relay-transaction.py` - This code could be stored in any online machine, no credentials is stored on this code other than a signed transaction which would be sent to an XRPL node for it to be validated on the ledger.
Preferably, `airgapped-wallet.py` should be on a Linux machine while `relay-transaction.py` could be on any operating system.
# Security Practices
Strongly note that an airgapped system's security is not determined by its code alone but the security practices that are being followed by an operator.
There are channels that can be maliciously used by outside parties to infiltrate an airgapped system and steal sensitive information.
There are other ways malware could interact across airgapped networks, but they all involve an infected USB drive or a similar device introducing malware onto the airgapped machine. They could also involve a person physically accessing the computer, compromising it and installing malware or modifying its hardware.
This is why it is also recommended to encrypt sensitive information being stored in an airgapped machine.
The airgapped machine should have a few rules enforced to close any possible channels getting abused to leak information outside of the machine:
### Wifi
- Disable any wireless networking hardware on the airgapped machine. For example, if you have a desktop PC with a Wifi card, open the PC and remove the Wifi hardware. If you cannot do that, you could go to the systems BIOS or UEFI firmware and disable the Wifi hardware.
### BlueTooth
- BlueTooth can be maliciously used by neighboring devices to steal data from an airgapped machine. It is recommended to remove or disable the BlueTooth hardware.
### USB
- The USB port can be used to transfer files in and out of the airgapped machine and this may act as a threat to an airgapped machine if the USB drive is infected with a malware. So after installing & setting up this airgapped wallet, it is highly recommended to block off all USB ports by using a USB blocker and not use them.
Do not reconnect the airgapped machine to a network, even when you need to transfer files! An effective airgapped machine should only serve 1 purpose, which is to store data and never open up a gateway for hackers to abuse and steal data.
# Tutorial
For testing purposes, you would need to have 2 machines and 1 phone in hand to scan the QR code.
1. 1st machine would be airgapped, following the security practices written [here](#security-practices). It stores and manages an XRPL Wallet.
2. 2nd machine would be a normal computer connected to the internet. It relays a signed transaction blob to a rippled node.
3. The phone would be used to scan a QR code, which contains a signed transaction blob. The phone would transmit it to the 2nd machine.
The diagram below shows you the process of submitting a transaction to the XRPL:
<p align="center">
<img src="https://user-images.githubusercontent.com/87929946/197970678-2a1b7f7e-d91e-424e-915e-5ba7d34689cc.png" width=75% height=75%>
</p>
# Setup
- Machine 1 - An airgapped computer (during setup, it must be connected to the internet to download the files)
- Machine 2 - A normal computer connected to the internet
- Phone - A normal phone with a working camera to scan a QR
## Machine 1 Setup
Since this machine will be airgapped, it is best to use Linux as the Operating System.
1. Install Python 3.8:
**Linux Command Line**:
```
sudo apt-get update
sudo apt-get install python3.8 python3-pip
```
**Website**: https://www.python.org/downloads/source/
2. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content/_code-samples/airgapped-wallet/py) directory
3. Import all the modules required by running:
```
pip install -r requirements.txt
```
4. Airgap the machine by following the security practices written [here](#security-practices).
5. Run `airgapped-wallet.py`
6. Scan the QR code and fund the account using the [testnet faucet](https://test.bithomp.com/faucet/)
7. Re-run the script and input '1' to generate a new transaction by following the instructions.
8. Use your phone to scan the QR code, then to send the signed transaction to Machine 2 for submission
## Machine 2 Setup
This machine will be used to transmit a signed transaction blob from Machine 1, it would require internet access.
1. Install Python 3.8
**Linux Command Line**:
```
sudo apt-get update
sudo apt-get install python3.8 python3-pip
```
**Website**: https://www.python.org/downloads/source/
2. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content/_code-samples/airgapped-wallet/py) directory
3. Import all the modules required by running:
```
pip install -r requirements.txt
```
4. Edit line 47 @ `relay-transaction.py` and insert the signed transaction blob from scanning the QR code Machine 1 generated.
5. Run `relay-transaction.py`
## Phone Setup
The phone requires a working camera that is able to scan a QR code and an internet connection for it to be able to transmit the signed transaction blob to Machine 2.
Once you have signed a transaction in the airgapped machine, a QR code will be generated which will contain the signed transaction blob. Example:
<img src="https://user-images.githubusercontent.com/87929946/196018292-f210a9f2-c5f8-412e-98c1-361a72286378.png" width=20% height=20%>
Scan the QR code using the phone and transmit it to Machine 2, which will then be sending it to a rippled node.
You can send a message to yourself using Discord, WhatsApp or even e-mail, then open up the message using Machine 2 to receive the signed transaction blob.

View File

@@ -0,0 +1,225 @@
import os
import base64
import qrcode
import platform
from PIL import Image
from pathlib import Path, PureWindowsPath, PurePath
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from xrpl import wallet
from xrpl.core import keypairs
from xrpl.utils import xrp_to_drops
from xrpl.models.transactions import Payment
from xrpl.transaction import safe_sign_transaction
def create_wallet(silent: False):
"""
Generates a keypair
"""
if not silent:
print("1. Generating seed...")
seed = keypairs.generate_seed()
print("2. Deriving keypair from seed...")
pub, priv = keypairs.derive_keypair(seed)
print("3. Deriving classic addresses from keypair..\n")
address = keypairs.derive_classic_address(pub)
else:
seed = keypairs.generate_seed()
pub, priv = keypairs.derive_keypair(seed)
address = keypairs.derive_classic_address(pub)
return address, seed
def sign_transaction(xrp_amount, destination, ledger_seq, wallet_seq, password):
"""
Signs transaction and returns signed transaction blob in QR code
"""
print("1. Retrieving encrypted private key and salt...")
with open(get_path("/WalletTEST/private.txt"), "r") as f:
seed = f.read()
seed = bytes.fromhex(seed)
with open(get_path("/WalletTEST/salt.txt"), "rb") as f:
salt = f.read()
print("2. Initializing key...")
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
iterations=100000,
salt=salt
)
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
crypt = Fernet(key)
print("3. Decrypting wallet's private key using password")
seed = crypt.decrypt(seed)
print("4. Initializing wallet using decrypted private key")
_wallet = wallet.Wallet(seed=seed.decode(), sequence=0)
validated_seq = ledger_seq
_wallet.sequence = wallet_seq
print("5. Constructing payment transaction...")
my_tx_payment = Payment(
account=_wallet.classic_address,
amount=xrp_to_drops(xrp=xrp_amount),
destination=destination,
last_ledger_sequence=validated_seq + 100,
# +100 to catch up with the ledger when we transmit the signed tx blob to Machine 2
sequence=_wallet.sequence,
fee="10"
)
print("6. Signing transaction...")
my_tx_payment_signed = safe_sign_transaction(transaction=my_tx_payment, wallet=_wallet)
img = qrcode.make(my_tx_payment_signed.to_dict())
print("7. Displaying signed transaction blob's QR code on the screen...")
img.save(get_path("/WalletTEST/transactionID.png"))
image = Image.open(get_path("/WalletTEST/transactionID.png"))
image.show()
print(f"RESULT: {my_tx_payment_signed.to_dict()}")
print("END RESULT: Successful")
def get_path(file):
"""
Get path (filesystem management)
"""
global File_
# Checks what OS is being used
OS = platform.system()
usr = Path.home()
# Get PATH format based on the OS
if OS == "Windows":
File_ = PureWindowsPath(str(usr) + file)
else: # Assuming Linux-style file format
File_ = PurePath(str(usr) + file)
return str(File_)
def create_wallet_directory():
global File, Path_
OS = platform.system()
usr = Path.home()
if OS == "Windows":
# If it's Windows, use this path:
print("- OS Detected: Windows")
File = PureWindowsPath(str(usr) + '/WalletTEST')
Path_ = str(PureWindowsPath(str(usr)))
if OS == "Linux":
print("- OS Detected: Linux")
# If it's Linux, use this path:
File = PurePath(str(usr) + '/WalletTEST')
Path_ = str(PurePath(str(usr)))
if not os.path.exists(File):
print("1. Generating wallet's keypair...")
pub, priv, seed = create_wallet(silent=True)
print("2. Creating wallet's file directory...")
os.makedirs(File)
print("3. Generating and saving public key's QR code...")
img = qrcode.make(pub)
img.save(get_path("/WalletTEST/public.png"))
print("4. Generating and saving wallet's salt...")
salt = os.urandom(16)
with open(get_path("/WalletTEST/salt.txt"), "wb") as f:
f.write(salt)
print("5. Generating wallet's filesystem password...")
password = "This is a unit test password 123 !@# -+= }{/"
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
iterations=100000,
salt=salt
)
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
crypt = Fernet(key)
print("6. Encrypting and saving private key by password...")
priv = crypt.encrypt(bytes(seed, encoding='utf-8'))
seed = crypt.encrypt(bytes(seed, encoding='utf-8'))
with open(get_path("/WalletTEST/seed.txt"), "w") as f:
f.write(seed.hex())
with open(get_path("/WalletTEST/private.txt"), "w") as f:
f.write(priv.hex())
with open(get_path("/WalletTEST/public.txt"), "w") as f:
f.write(pub)
if os.path.exists(File):
print(f"0. Wallet's filesystem already exist as the unit test has been performed before. Directory: {File}")
def showcase_wallet_address_qr_code():
with open(get_path("/WalletTEST/public.txt"), "r") as f:
print(f"0. Wallet Address: {f.read()}")
__path = get_path("/WalletTEST/public.png")
print(f"1. Getting address from {__path}...")
print("2. Displaying QR code on the screen...")
image = Image.open(get_path("/Wallet/public.png"))
image.show()
if __name__ == '__main__':
print("Airgapped Machine Unit Test (5 functions):\n")
print(f"UNIT TEST 1. create_wallet():")
_address, _private, _seed = create_wallet(silent=False)
print(f"-- RESULTS --\n"
f"Address: {_address}\n"
f"Private Key: {_private}\n"
f"Seed: {_seed}\n"
f"END RESULT: Successful"
)
print(f"\nUNIT TEST 2. create_wallet_directory():")
create_wallet_directory()
print("RESULT: Successful")
print("\nUNIT TEST 3. showcase_wallet_address_qr_code():")
showcase_wallet_address_qr_code()
print("RESULT: Successful")
print("\nUNIT TEST 4. get_path():")
print("1. Getting files' path...\n")
txt_file = get_path("/WalletTEST/FILE123.txt")
png_file = get_path("/WalletTEST/PIC321.png")
print(f"-- RESULTS --\n"
f"txt_file: {txt_file}\n"
f"png_file: {png_file}\n"
f"END RESULT: Successful")
print("\nUNIT TEST 5. sign_transaction():")
print("Parameters: xrp_amount, destination, ledger_seq, wallet_seq, password")
sign_transaction(
xrp_amount=10,
destination="rPEpirdT9UCNbnaZMJ4ENwKAwJqrTpvgMQ",
ledger_seq=32602000,
wallet_seq=32600100,
password="This is a unit test password 123 !@# -+= }{/"
)

View File

@@ -0,0 +1,227 @@
import os
import shutil
import base64
import qrcode
import platform
from PIL import Image
from pathlib import Path, PureWindowsPath, PurePath
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from xrpl import wallet
from xrpl.core import keypairs
from xrpl.utils import xrp_to_drops
from xrpl.models.transactions import Payment
from xrpl.transaction import safe_sign_transaction
def create_wallet():
"""
Generates a keypair
"""
seed = keypairs.generate_seed()
pub, priv = keypairs.derive_keypair(seed)
address = keypairs.derive_classic_address(pub)
print(
f"\n\n XRP WALLET CREDENTIALS"
f"\n Wallet Address: {address}"
f"\n Seed: {seed}"
)
return address, seed
def sign_transaction(_xrp_amount, _destination, _ledger_seq, _wallet_seq, password):
"""
Signs transaction and returns signed transaction blob in QR code
"""
with open(get_path("/Wallet/private.txt"), "r") as f:
_seed = f.read()
_seed = bytes.fromhex(_seed)
with open(get_path("/Wallet/salt.txt"), "rb") as f:
salt = f.read()
# Line 49-58: initialize key
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
iterations=100000,
salt=salt
)
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
crypt = Fernet(key)
# Decrypts the wallet's private key
_seed = crypt.decrypt(_seed)
_wallet = wallet.Wallet(seed=_seed.decode(), sequence=0)
validated_seq = _ledger_seq
_wallet.sequence = _wallet_seq
# Construct Payment transaction
my_tx_payment = Payment(
account=_wallet.classic_address,
amount=xrp_to_drops(xrp=_xrp_amount),
destination=_destination,
last_ledger_sequence=validated_seq + 100,
# +100 to catch up with the ledger when we transmit the signed tx blob to Machine 2
sequence=_wallet.sequence,
fee="10"
)
# Signs transaction and displays the signed_tx blob in QR code
# Scan the QR code and transmit the signed_tx blob to an online machine (Machine 2) to relay it to the XRPL
my_tx_payment_signed = safe_sign_transaction(transaction=my_tx_payment, wallet=_wallet)
img = qrcode.make(my_tx_payment_signed.to_dict())
img.save(get_path("/Wallet/transactionID.png"))
image = Image.open(get_path("/Wallet/transactionID.png"))
image.show()
def get_path(file):
"""
Get path (filesystem management)
"""
global File_
# Checks what OS is being us
OS = platform.system()
usr = Path.home()
# Get PATH format based on the OS
if OS == "Windows":
File_ = PureWindowsPath(str(usr) + file)
else: # Assuming Linux-style file format, use this path:
File_ = PurePath(str(usr) + file)
return str(File_)
def main():
global File, Path_
# Gets the machine's operating system (OS)
OS = platform.system()
usr = Path.home()
if OS == "Windows":
# If it's Windows, use this path:
File = PureWindowsPath(str(usr) + '/Wallet')
Path_ = str(PureWindowsPath(str(usr)))
else: # Assuming Linux-style file format, use this path:
File = PurePath(str(usr) + '/Wallet')
Path_ = str(PurePath(str(usr)))
# If the Wallet's folder already exists, continue on
if os.path.exists(File) and os.path.exists(get_path("/Wallet/public.txt")):
while True:
ask = int(input("\n 1. Transact XRP"
"\n 2. Generate an XRP wallet (read only)"
"\n 3. Showcase XRP Wallet Address (QR Code)"
"\n 4. Exit"
"\n\n Enter Index: "
))
if ask == 1:
password = str(input(" Enter Password: "))
amount = float(input("\n Enter XRP To Send: "))
destination = input("If you just want to try it out, you can use the faucet account rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
"\n Enter Destination: ")
wallet_sequence = int(input("Look up the 'Next Sequence' for the account using test.bithomp.com and enter it below!"
"\n Enter Wallet Sequence: "))
ledger_sequence = int(input("Look up the latest ledger sequence on testnet.xrpl.org and enter it below!"
"\n Enter Ledger Sequence: "))
sign_transaction(_xrp_amount=amount,
_destination=destination,
_ledger_seq=ledger_sequence,
_wallet_seq=wallet_sequence,
password=password
)
del destination, amount, wallet_sequence, ledger_sequence
if ask == 2:
_pub, _seed = create_wallet()
if ask == 3:
with open(get_path("/Wallet/public.txt"), "r") as f:
print(f"\n Wallet Address: {f.read()}")
image = Image.open(get_path("/Wallet/public.png"))
image.show()
if ask == 4:
return 0
else:
# If the Wallet's folder does not exist, create one and store wallet data (encrypted private key, encrypted seed, account address)
# If the Wallet's directory exists but files are missing, delete it and generate a new wallet
if os.path.exists(File):
confirmation = input(f"We've detected missing files on {File}, would you like to delete your wallet's credentials & generate new wallet credentials? (YES/NO):")
if confirmation == "YES":
confirmation_1 = input(f"All wallet credentials will be lost if you continue, are you sure? (YES/NO): ")
if confirmation_1 == "YES":
shutil.rmtree(File)
else:
print("Aborted: Wallet credentials are still intact")
return 0
else:
print("- Wallet credentials are still intact")
return 0
os.makedirs(File)
pub, seed = create_wallet()
img = qrcode.make(pub)
img.save(get_path("/Wallet/public.png"))
print("\nCreating a brand new Wallet, please enter a new password")
password = str(input("\n Enter Password: "))
salt = os.urandom(16)
with open(get_path("/Wallet/salt.txt"), "wb") as f:
f.write(salt)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
iterations=100000,
salt=salt
)
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
crypt = Fernet(key)
priv = crypt.encrypt(bytes(seed, encoding='utf-8'))
seed = crypt.encrypt(bytes(seed, encoding='utf-8'))
with open(get_path("/Wallet/seed.txt"), "w") as f:
f.write(seed.hex())
with open(get_path("/Wallet/private.txt"), "w") as f:
f.write(priv.hex())
with open(get_path("/Wallet/public.txt"), "w") as f:
f.write(pub)
openimg = Image.open(get_path("/Wallet/public.png"))
openimg.show()
print("\nFinished generating an account.")
print(f"\nWallet Address: {pub}")
print("\nPlease scan the QR code on your phone and use https://test.bithomp.com/faucet/ to fund the account."
"\nAfter that, you're able to sign transactions and transmit them to Machine 2 (online machine).")
# Loop back to the start after setup
main()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,50 @@
from xrpl.clients import JsonRpcClient
from xrpl.models.transactions import Payment
from xrpl.transaction import send_reliable_submission
def connect_node(_node):
"""
Connects to a node
"""
JSON_RPC_URL = _node
_client = JsonRpcClient(url=JSON_RPC_URL)
print("\n --- Connected to Node")
return _client
def send_transaction(transaction_dict):
"""
Connects to a node -> Send Transaction
Main Function to send transaction to the XRPL
"""
client = connect_node("https://s.altnet.rippletest.net:51234/")
# TESTNET: "https://s.altnet.rippletest.net:51234/"
# MAINNET: "https://s2.ripple.com:51234/"
# Since we manually inserted the tx blob, we need to initialize it into a Payment so xrpl-py could process it
my_tx_signed = Payment.from_dict(transaction_dict)
tx = send_reliable_submission(transaction=my_tx_signed, client=client)
tx_hash = tx.result['hash']
tx_destination = tx.result['Destination']
tx_xrp_amount = int(tx.result['Amount']) / 1000000
tx_account = tx.result['Account']
print(f"\n XRPL Explorer: https://testnet.xrpl.org/transactions/{tx_hash}"
f"\n Transaction Hash: {tx_hash}"
f"\n Transaction Destination: {tx_destination}"
f"\n Transacted XRP: {tx_xrp_amount}"
f"\n Wallet Used: {tx_account}"
)
if __name__ == '__main__':
tx_blob = "ENTER TX BLOB HERE"
if tx_blob == "ENTER TX BLOB HERE":
print("Set tx to 'tx_blob' received from scanning the QR code generated by the airgapped wallet")
else:
send_transaction(tx_blob)

View File

@@ -0,0 +1,26 @@
anyio==3.2.1
asgiref==3.4.1
base58==2.1.0
certifi==2021.5.30
cffi==1.15.0
colorama==0.4.4
cryptography==35.0.0
Django==3.2.6
ECPy==1.2.5
h11==0.12.0
httpcore==0.13.6
httpx==0.18.2
idna==3.2
image==1.5.33
pifacedigitalio==3.0.5
Pillow==8.3.1
pycparser==2.20
pytz==2021.1
qrcode==7.2
rfc3986==1.5.0
six==1.16.0
sniffio==1.2.0
sqlparse==0.4.1
typing-extensions==3.10.0.0
websockets==9.1
xrpl-py==1.1.1