photon-lib
A high level library for building bitcoin wallets with react native.
Scope
Provide an easy-to-use high level api for the following:
- hd wallets (bech32 and p2sh)
- multisig support
- an electrum light client
- secure enclave backed key storage on iOS and Android (where available)
- encrypted key backup on iCloud/GDrive + 2FA (see photon-keyserver)
Demo
Threat model
Please see the threat model doc for a discussion about attack vectors and mitigation strategies.
Usage
In your react-native app...
Installing
Make sure to install all peer dependencies:
npm install --save @photon-sdk/photon-lib react-native-randombytes react-native-keychain @photon-sdk/react-native-icloudstore @react-native-async-storage/async-storage @photon-sdk/react-native-tcp @react-native-google-signin/google-signin @robinbobin/react-native-google-drive-api-wrapper node-libs-react-native react-native-device-info
Update cocoapods
npx pod-install
Configure Xcode project
In your target's "capabilities" tab in Xcode, make sure that iCloud is switched on as well as make sure that the "Key-value storage" option is checked.
Wire up node libs
Follow the usage instructions for node-libs-react-native.
Sample app
An example app using photon-lib can be found here photon-sdk/photon-app.
This PR shows what the diff should look like when installing photon-lib to your react-native app.
Example
Init Key Server
First we'll need to tell the key backup module which key server to use. See photon-sdk/photon-keyserver for how to deploy a key server instance for your app.
import { KeyBackup } from '@photon-sdk/photon-lib';
KeyBackup.init({
keyServerURI: 'http://localhost:3000'
});
Authenticate Cloud Storage
The encrypted backup is stored on the user's cloud storage account. On Android the user is required to grant access to an app specific Google Drive folder with an OAuth dialog. For iOS apps this step can be ignored as iCloud does not require extra authentication.
await KeyBackup.authenticate({
clientId: '<FROM DEVELOPER CONSOLE>'
});
Key Backup
Now let's do an encrypted backup of a user's wallet to their iCloud account. The encryption key will be stored on your app's key server. A random Key ID
(stored automatically on the user's iCloud) and a user chosen PIN
is used for authentication with the key server.
import { HDSegwitBech32Wallet, KeyBackup } from '@photon-sdk/photon-lib';
const wallet = new HDSegwitBech32Wallet();
await wallet.generate();
const mnemonic = await wallet.getSecret();
const data = { mnemonic };
const pin = '1234';
await KeyBackup.createBackup({ data, pin });
Key Restore
Now let's restore the user's wallet on their new device. This will download their encrypted mnemonic from iCloud and decrypt it using the encryption key from the key server. The random Key ID
(stored on the user's iCloud) and the PIN
that was set during wallet backup will be used to authenticate with the key server. N.B. encryption key download is locked for 7 days after 10 failed authentication attempts to mitigate brute forcing of the PIN.
import { HDSegwitBech32Wallet, KeyBackup, WalletStore } from '@photon-sdk/photon-lib';
const exists = await KeyBackup.checkForExistingBackup();
if (!exists) return;
const pin = '1234';
const data = await KeyBackup.restoreBackup({ pin });
const wallet = new HDSegwitBech32Wallet();
wallet.setSecret(data.mnemonic);
const store = new WalletStore();
store.wallets.push(wallet);
await store.saveToDisk();
Change the PIN
Users can change the authentication PIN simply by calling the following api. A PIN must be at least 4 digits, but can also be a complex passphrase up to 256 chars in length.
import { KeyBackup } from '@photon-sdk/photon-lib';
const pin = '1234';
const newPin = 'complex passphrases are also possible';
await KeyBackup.changePin({ pin, newPin });
Add Recovery Phone Number (optional)
In order to allow for wallet recovery in case the user forgets their PIN, a recovery phone number can be set. A 30 day time delay is enforced for PIN recovery to mitigate SIM swap attacks. The phone number is stored in plaintext only on the user's iCloud. A hash of the phone number is stored on the key server for authentication (hashed with scrypt and a random salt).
import { KeyBackup } from '@photon-sdk/photon-lib';
const userId = '+4917512345678';
const pin = '1234';
await KeyBackup.registerPhone({ userId, pin });
const code = '000000';
await KeyBackup.verifyPhone({ userId, code });
Add Recovery Email Address (optional)
In order to allow for wallet recovery in case the user forgets their PIN, a recovery email address can be set. A 30 day time delay is enforced for PIN recovery to mitigate SIM swap attacks. The email address is stored in plaintext only on the user's iCloud. A hash of the email address is stored on the key server for authentication (hashed with scrypt and a random salt).
import { KeyBackup } from '@photon-sdk/photon-lib';
const userId = 'jon@example.com';
const pin = '1234';
await KeyBackup.registerEmail({ userId, pin });
const code = '000000';
await KeyBackup.verifyEmail({ userId, code });
Reset the PIN via Recovery Email Address (works the same via phone)
In case the user forgets their PIN, apps should encourage users to set a recovery phone number or email address during sign up. This can be used later to reset the PIN with a 30 day time delay.
import { KeyBackup } from '@photon-sdk/photon-lib';
const userId = await KeyBackup.getEmail()
await KeyBackup.initPinReset({ userId });
const code = '123456';
const newPin = '5678';
const delay = await KeyBackup.verifyPinReset({ userId, code, newPin });
if (delay) {
return
}
await KeyBackup.initPinReset({ userId });
const code = '654321';
await KeyBackup.verifyPinReset({ userId, code, newPin });
const pin = '5678';
const data = await KeyBackup.restoreBackup({ pin });
Init Electrum Client
First we'll need to init the electrum client by specifying the host and port of our full node.
import { ElectrumClient } from '@photon-sdk/photon-lib';
const options = {
host: 'blockstream.info',
ssl: '700'
};
await ElectrumClient.connectMain(options);
await ElectrumClient.waitTillConnected();
Wallet Balance & Transaction Data
Now we'll generate a new wallet key, store it securely in the device keychain and fetch transactions and balances using the electrum client.
import { HDSegwitBech32Wallet, WalletStore } from '@photon-sdk/photon-lib';
const wallet = new HDSegwitBech32Wallet();
await wallet.generate();
const store = new WalletStore();
store.wallets.push(wallet);
await store.saveToDisk();
await store.fetchWalletBalances();
await store.fetchWalletTransactions();
const balance = store.getBalance();
const address = await wallet.getAddressAsync();
Create & Broadcast Transaction
Finally we'll fetch the wallets utxos, create a new transaction, and broadcast it using the electrum client.
import { HDSegwitBech32Wallet, WalletStore } from '@photon-sdk/photon-lib';
const wallet = new HDSegwitBech32Wallet();
await wallet.generate();
await wallet.fetchUtxo();
const utxo = wallet.getUtxo();
const target = [{
value: 1000,
address: 'some-address'
}];
const feeRate = 1;
const changeTo = await wallet.getAddressAsync();
const newTx = wallet.createTransaction(utxo, target, feeRate, changeTo);
await wallet.broadcastTx(newTx.tx.toHex());
Create Multisig Wallet & cosign PSBT
In this example we'll create a 2-of-2 multisig wallet. Cosigners can be added as either xpubs or mnemonics. Once created, the wallet can be interacted with using the same apis as above.
import { MultisigHDWallet, WalletStore } from '@photon-sdk/photon-lib';
const path = "m/48'/0'/0'/2'";
const key1_mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const key2_fp = '05C0D4E1';
const key2_zpub = 'Zpub755JaEN81qADr1Hq22Q6AbiRutDnCMdWghxUrpxkPB5JhdcAzWzQGMiSS58oxEjTqZkxBJ1q6TwvQ1EkiNEsrD18aeVnuJgEDjg1S3ETtd6';
const wallet = new MultisigHDWallet();
wallet.addCosigner(key1_mnemonic);
wallet.addCosigner(key2_zpub, key2_fp);
wallet.setDerivationPath(path);
wallet.setM(2);
const newTx = wallet.createTransaction(utxo, target, feeRate, changeTo);
const signedTx = wallet.cosignPsbt(newTx.psbt);
await wallet.broadcastTx(signedTx.tx.toHex());
Development and testing
Clone the git repo and then:
npm install && npm test
Credit