diff --git a/content/_code-samples/airgapped-wallet/js/.gitignore b/content/_code-samples/airgapped-wallet/js/.gitignore new file mode 100644 index 0000000000..5e5994e3b0 --- /dev/null +++ b/content/_code-samples/airgapped-wallet/js/.gitignore @@ -0,0 +1 @@ +Wallet/ \ No newline at end of file diff --git a/content/_code-samples/airgapped-wallet/js/README.md b/content/_code-samples/airgapped-wallet/js/README.md new file mode 100644 index 0000000000..080eccd85d --- /dev/null +++ b/content/_code-samples/airgapped-wallet/js/README.md @@ -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: +

+ +

+ +# 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: + + + +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 diff --git a/content/_code-samples/airgapped-wallet/js/airgapped-wallet.js b/content/_code-samples/airgapped-wallet/js/airgapped-wallet.js new file mode 100644 index 0000000000..22f8b623c5 --- /dev/null +++ b/content/_code-samples/airgapped-wallet/js/airgapped-wallet.js @@ -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} + */ +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() \ No newline at end of file diff --git a/content/_code-samples/airgapped-wallet/js/package.json b/content/_code-samples/airgapped-wallet/js/package.json new file mode 100644 index 0000000000..e05e42ebe0 --- /dev/null +++ b/content/_code-samples/airgapped-wallet/js/package.json @@ -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" + } +} diff --git a/content/_code-samples/airgapped-wallet/js/relay-transaction.js b/content/_code-samples/airgapped-wallet/js/relay-transaction.js new file mode 100644 index 0000000000..4f5cfe3f1c --- /dev/null +++ b/content/_code-samples/airgapped-wallet/js/relay-transaction.js @@ -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() \ No newline at end of file