mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-19 11:15:49 +00:00
Bounties 0077 js samples 2 Airgapped Wallet (#1693)
This commit is contained in:
1
content/_code-samples/airgapped-wallet/js/.gitignore
vendored
Normal file
1
content/_code-samples/airgapped-wallet/js/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Wallet/
|
||||
92
content/_code-samples/airgapped-wallet/js/README.md
Normal file
92
content/_code-samples/airgapped-wallet/js/README.md
Normal 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 system’s 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
|
||||
223
content/_code-samples/airgapped-wallet/js/airgapped-wallet.js
Normal file
223
content/_code-samples/airgapped-wallet/js/airgapped-wallet.js
Normal 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()
|
||||
18
content/_code-samples/airgapped-wallet/js/package.json
Normal file
18
content/_code-samples/airgapped-wallet/js/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user