Bounties 0077 js samples 2 Airgapped Wallet (#1693)

This commit is contained in:
AlexanderBuzz
2023-02-08 18:35:25 +01:00
committed by GitHub
parent fa05d0ead7
commit c8581ffbae
5 changed files with 371 additions and 0 deletions

View File

@@ -0,0 +1 @@
Wallet/

View File

@@ -0,0 +1,92 @@
# 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.js` - 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.js` - 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.js` should be on a Linux machine while `relay-transaction.js` 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. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content/_code-samples/airgapped-wallet/js) directory
2. Import all the modules required by running: `npm install`
3. Airgap the machine by following the security practices written [here](#security-practices).
4. Run `node airgapped-wallet.js`
5. Scan the QR code and fund the account using the [testnet faucet](https://test.bithomp.com/faucet/)
6. Re-run the script and input '1' to generate a new transaction by following the instructions.
7. Use your phone to scan the QR code, then to send the signed transaction to Machine 2 for submission
## 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, copy it to the clipboard, 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.
## Machine 2 Setup
This machine will be used to transmit a signed transaction blob from Machine 1, it would require internet access.
1. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content/_code-samples/airgapped-wallet/js) directory
2. Import all the modules required by running `npm install`
3. Run `relay-transaction.js` and copy-and-paste the received output of Machine 1 when prompted

View File

@@ -0,0 +1,223 @@
const crypto = require("crypto")
const fs = require('fs')
const fernet = require("fernet");
const open = require('open');
const path = require('path')
const prompt = require('prompt')
const { generateSeed, deriveAddress, deriveKeypair } = require("ripple-keypairs/dist/")
const QRCode = require('qrcode')
const xrpl = require('xrpl')
const demoAccountSeed = 'sskwYQmxT7SA37ceRaGXA5PhQYrDS'
const demoAccountAddress = 'rEDd3Wy76Ta1WqfDP2DcnBKHu31SpSiUQrS'
const demoDestinationSeed = 'sEdVokfq7fVXXjZTii2WhtpqGbJni6s'
const demoDestinationAddress = 'rBgNowfkmPczhMjHRYnBPsuSodDHWHQLdj'
const FEE = '12'
const LEDGER_OFFSET = 300
const WALLET_DIR = 'Wallet'
/**
* Generates a new (unfunded) wallet
*
* @returns {{address: *, seed: *}}
*/
createWallet = function () {
const seed = generateSeed()
const {publicKey, privateKey} = deriveKeypair(seed)
const address = deriveAddress(publicKey)
console.log(
"XRP Wallet Credentials " +
"Wallet Address: " + address +
"Seed: " + seed
)
return {address, seed}
}
/**
* Signs transaction and returns signed transaction blob in QR code
*
* @param xrpAmount
* @param destination
* @param ledgerSequence
* @param walletSequence
* @param password
* @returns {Promise<void>}
*/
signTransaction = async function (xrpAmount, destination, ledgerSequence, walletSequence, password) {
const salt = fs.readFileSync(path.join(__dirname, WALLET_DIR , 'salt.txt')).toString()
const encodedSeed = fs.readFileSync(path.join(__dirname, WALLET_DIR , 'seed.txt')).toString()
// Hashing salted password using Password-Based Key Derivation Function 2
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 32, 'sha256')
// Generate a Fernet secret we can use for symmetric encryption
const secret = new fernet.Secret(derivedKey.toString('base64'));
// Generate decryption token
const token = new fernet.Token({
secret: secret,
token: encodedSeed,
ttl: 0
})
const seed = token.decode();
const wallet = xrpl.Wallet.fromSeed(seed)
const paymentTx = {
'TransactionType': 'Payment',
'Account': wallet.classicAddress,
'Amount': xrpl.xrpToDrops(xrpAmount),
'Destination': destination
}
// Normally we would fetch certain needed values like Fee,
// LastLedgerSequence snd programmatically, like so:
//
// const preparedTx = await client.autofill(paymentTx)
//
// But since this is an airgapped wallet without internet
// connection, we have to do it manually:
//
// paymentTx.Sequence is set in setNextValidSequenceNumber() via sugar/autofill
// paymentTx.LastLedgerSequence is set in setLatestValidatedLedgerSequence() via sugar/autofill
// paymentTx.Fee is set in getFeeXrp() via sugar/getFeeXrp
paymentTx.Sequence = walletSequence
paymentTx.LastLedgerSequence = ledgerSequence + LEDGER_OFFSET
paymentTx.Fee = FEE
const signedTx = wallet.sign(paymentTx)
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'tx_blob.txt'), signedTx.tx_blob)
QRCode.toFile(path.join(__dirname, WALLET_DIR , 'tx_blob.png'), signedTx.tx_blob)
open(path.join(__dirname, WALLET_DIR , 'tx_blob.png'))
}
main = async function () {
if (!fs.existsSync(WALLET_DIR )) {
// Create Wallet directory in case it does not exist yet
fs.mkdirSync(path.join(__dirname, WALLET_DIR ));
}
if (!fs.existsSync(path.join(__dirname, WALLET_DIR , 'address.txt'))) {
// Generate a new (unfunded) Wallet
const {address, seed} = createWallet()
prompt.start();
const {password} = await prompt.get([{
name: 'password',
description: 'Creating a brand new Wallet, please enter a new password \n Enter Password:',
type: 'string',
required: true
}])
prompt.stop();
const salt = crypto.randomBytes(20).toString('hex')
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'salt.txt'), salt);
// Hashing salted password using Password-Based Key Derivation Function 2
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 32, 'sha256')
// Generate a Fernet secret we can use for symmetric encryption
const secret = new fernet.Secret(derivedKey.toString('base64'));
// Generate encryption token with secret, time and initialization vector
// In a real-world use case we would have current time and a random IV,
// but for demo purposes being deterministic is just fine
const token = new fernet.Token({
secret: secret,
time: Date.parse(1),
iv: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
})
const privateKey = token.encode(seed)
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'seed.txt'), privateKey)
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'address.txt'), address)
QRCode.toFile(path.join(__dirname, WALLET_DIR , 'address.png'), address)
console.log(''
+ 'Finished generating an account.\n'
+ 'Wallet Address: ' + address + '\n'
+ 'Please scan the QR code on your phone and use https://test.bithomp.com/faucet/ to fund the account.\n'
+ 'After that, you\'re able to sign transactions and transmit them to Machine 2 (online machine).')
return
}
prompt.start();
console.log(''
+ '1. Transact XRP.\n'
+ '2. Generate an XRP wallet (read only)\n'
+ '3. Showcase XRP Wallet Address (QR Code)\n'
+ '4. Exit')
const {menu} = await prompt.get([{
name: 'menu',
description: 'Enter Index:',
type: 'integer',
required: true
}])
if (menu === 1) {
const {
password,
xrpAmount,
destinationAddress,
accountSequence,
ledgerSequence
} = await prompt.get([{
name: 'password',
description: 'Enter Password',
type: 'string',
required: true
}, {
name: 'xrpAmount',
description: 'Enter XRP To Send',
type: 'number',
required: true
}, {
name: 'destinationAddress',
description: 'If you just want to try it out, you can use the faucet account rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe. Enter Destination',
type: 'string',
required: true
}, {
name: 'accountSequence',
description: 'Look up the \'Next Sequence\' for the account using test.bithomp.com and enter it',
type: 'integer',
required: true
}, {
name: 'ledgerSequence',
description: 'Look up the latest ledger sequence on testnet.xrpl.org and enter it below!',
type: 'integer',
required: true
}])
await signTransaction(xrpAmount, destinationAddress, ledgerSequence, accountSequence, password)
} else if (menu === 2) {
const {address, seed} = createWallet()
console.log('Generated readonly Wallet (address: ' + address + ' seed: ' + seed + ')')
} else if (menu === 3) {
const address = fs.readFileSync(path.join(__dirname, WALLET_DIR , 'address.txt')).toString()
console.log('Wallet Address: ' + address)
open(path.join(__dirname, WALLET_DIR , 'address.png'))
} else {
return
}
prompt.stop();
}
main()

View File

@@ -0,0 +1,18 @@
{
"name": "airgapped-wallet",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"fernet": "^0.4.0",
"open": "^8.4.0",
"pbkdf2-hmac": "^1.1.0",
"prompt": "^1.3.0",
"qrcode": "^1.5.1",
"xrpl": "^2.0.0"
},
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
}

View File

@@ -0,0 +1,37 @@
const prompt = require('prompt')
const xrpl = require('xrpl')
sendTransaction = async function (tx_blob) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
console.log("Connected to node")
const tx = await client.submitAndWait(tx_blob)
const txHash = tx.result.hash
const txDestination = tx.result.Destination
const txXrpAmount = xrpl.dropsToXrp(tx.result.Amount)
const txAccount = tx.result.Account
console.log("XRPL Explorer: https://testnet.xrpl.org/transactions/" + txHash)
console.log("Transaction Hash: " + txHash)
console.log("Transaction Destination: " + txDestination)
console.log("XRP sent: " + txXrpAmount)
console.log("Wallet used: " + txAccount)
await client.disconnect()
}
main = async function () {
const {tx_blob} = await prompt.get([{
name: 'tx_blob',
description: 'Set tx to \'tx_blob\' received from scanning the QR code generated by the airgapped wallet',
type: 'string',
required: true
}])
await sendTransaction(tx_blob)
}
main()