Skip to content

Commit

Permalink
ADD: tool to generate last mnemonic word (#5722)
Browse files Browse the repository at this point in the history
  • Loading branch information
3bitcoins committed Nov 2, 2023
1 parent bd986d5 commit 53271d1
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import WalletAddresses from './screen/wallets/addresses';
import ReorderWallets from './screen/wallets/reorderWallets';
import SelectWallet from './screen/wallets/selectWallet';
import ProvideEntropy from './screen/wallets/provideEntropy';
import GenerateWord from './screen/wallets/generateWord';

import TransactionDetails from './screen/transactions/details';
import TransactionStatus from './screen/transactions/transactionStatus';
Expand Down Expand Up @@ -145,6 +146,7 @@ const WalletsRoot = () => {
/>
<WalletsStack.Screen name="Broadcast" component={Broadcast} options={Broadcast.navigationOptions(theme)} />
<WalletsStack.Screen name="IsItMyAddress" component={IsItMyAddress} options={IsItMyAddress.navigationOptions(theme)} />
<WalletsStack.Screen name="GenerateWord" component={GenerateWord} options={GenerateWord.navigationOptions(theme)} />
<WalletsStack.Screen name="LnurlPay" component={LnurlPay} options={LnurlPay.navigationOptions(theme)} />
<WalletsStack.Screen name="LnurlPaySuccess" component={LnurlPaySuccess} options={LnurlPaySuccess.navigationOptions(theme)} />
<WalletsStack.Screen name="LnurlAuth" component={LnurlAuth} options={LnurlAuth.navigationOptions(theme)} />
Expand Down
79 changes: 79 additions & 0 deletions blue_modules/checksumWords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as bip39 from 'bip39';
import createHash from 'create-hash';

// partial (11 or 23 word) seed phrase
export function generateChecksumWords(stringSeedPhrase) {
const seedPhrase = stringSeedPhrase.toLowerCase().trim().split(' ');

if ((seedPhrase.length + 1) % 3 > 0) {
return false; // Partial mnemonic size must be multiple of three words, less one.
}

const wordList = bip39.wordlists[bip39.getDefaultWordlist()];

const concatLenBits = seedPhrase.length * 11;
const concatBits = new Array(concatLenBits);
let wordindex = 0;
for (let i = 0; i < seedPhrase.length; i++) {
const word = seedPhrase[i];
const ndx = wordList.indexOf(word.toLowerCase());
if (ndx === -1) return false;
// Set the next 11 bits to the value of the index.
for (let ii = 0; ii < 11; ++ii) {
concatBits[wordindex * 11 + ii] = (ndx & (1 << (10 - ii))) !== 0; // eslint-disable-line no-bitwise
}
++wordindex;
}

const checksumLengthBits = (concatLenBits + 11) / 33;
const entropyLengthBits = concatLenBits + 11 - checksumLengthBits;
const varyingLengthBits = entropyLengthBits - concatLenBits;
const numPermutations = 2 ** varyingLengthBits;

const bitPermutations = new Array(numPermutations);

for (let i = 0; i < numPermutations; i++) {
if (bitPermutations[i] === undefined || bitPermutations[i] === null) bitPermutations[i] = new Array(varyingLengthBits);
for (let j = 0; j < varyingLengthBits; j++) {
bitPermutations[i][j] = ((i >> j) & 1) === 1; // eslint-disable-line no-bitwise
}
}

const possibleWords = [];
for (let i = 0; i < bitPermutations.length; i++) {
const bitPermutation = bitPermutations[i];
const entropyBits = new Array(concatLenBits + varyingLengthBits);
entropyBits.splice(0, 0, ...concatBits);
entropyBits.splice(concatBits.length, 0, ...bitPermutation.slice(0, varyingLengthBits));

const entropy = new Array(entropyLengthBits / 8);
for (let ii = 0; ii < entropy.length; ++ii) {
for (let jj = 0; jj < 8; ++jj) {
if (entropyBits[ii * 8 + jj]) {
entropy[ii] |= 1 << (7 - jj); // eslint-disable-line no-bitwise
}
}
}

const hash = createHash('sha256').update(Buffer.from(entropy)).digest();

const hashBits = new Array(hash.length * 8);
for (let iq = 0; iq < hash.length; ++iq) for (let jq = 0; jq < 8; ++jq) hashBits[iq * 8 + jq] = (hash[iq] & (1 << (7 - jq))) !== 0; // eslint-disable-line no-bitwise

const wordBits = new Array(11);
wordBits.splice(0, 0, ...bitPermutation.slice(0, varyingLengthBits));
wordBits.splice(varyingLengthBits, 0, ...hashBits.slice(0, checksumLengthBits));

let index = 0;
for (let j = 0; j < 11; ++j) {
index <<= 1; // eslint-disable-line no-bitwise
if (wordBits[j]) {
index |= 0x1; // eslint-disable-line no-bitwise
}
}

possibleWords.push(wordList[index]);
}

return possibleWords;
}
6 changes: 6 additions & 0 deletions loc/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,12 @@
"no_wallet_owns_address": "None of the available wallets own the provided address.",
"view_qrcode": "View QRCode"
},
"autofill_word": {
"title": "Generate final mnemonic word",
"enter": "Enter partial mnemonic phrase",
"generate_word": "Generate final word",
"error": "Input is not an 11 or 23 word partial mnemonic!"
},
"cc": {
"change": "Change",
"coins_selected": "Coins Selected ({number})",
Expand Down
5 changes: 5 additions & 0 deletions screen/settings/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ const NetworkSettings = () => {
navigate('Broadcast');
};

const navigateToGenerateWord = () => {
navigate('GenerateWord');
};

return (
<SafeBlueArea>
<ScrollView>
<BlueListItem title={loc.is_it_my_address.title} onPress={navigateToIsItMyAddress} testID="IsItMyAddress" chevron />
<BlueListItem title={loc.settings.network_broadcast} onPress={navigateToBroadcast} testID="Broadcast" chevron />
<BlueListItem title={loc.autofill_word.title} onPress={navigateToGenerateWord} testID="GenerateWord" chevron />
</ScrollView>
</SafeBlueArea>
);
Expand Down
138 changes: 138 additions & 0 deletions screen/wallets/generateWord.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useState } from 'react';
import { useTheme } from '@react-navigation/native';
import { StyleSheet, View, KeyboardAvoidingView, Platform, TextInput, Keyboard } from 'react-native';

import loc from '../../loc';
import { BlueButton, BlueCard, BlueSpacing10, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';

import { randomBytes } from '../../class/rng';
import { generateChecksumWords } from '../../blue_modules/checksumWords';

const GenerateWord = () => {
const { colors } = useTheme();

const [mnemonic, setMnemonic] = useState('');
const [result, setResult] = useState('');

const stylesHooks = StyleSheet.create({
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
});

const handleUpdateMnemonic = nextValue => {
setMnemonic(nextValue);
setResult();
};

const checkMnemonic = async () => {
Keyboard.dismiss();

const seedPhrase = mnemonic.toString();

const possibleWords = generateChecksumWords(seedPhrase);

if (!possibleWords) {
// likely because of an invalid mnemonic
setResult(loc.autofill_word.error);
return;
}

const random = await randomBytes(1);
const randomindex = Math.round((random.readUInt8(0) / 255) * (possibleWords.length - 1));

setResult(possibleWords[randomindex]);
};

const clearMnemonicInput = () => {
setMnemonic('');
setResult();
};

return (
<SafeBlueArea style={styles.blueArea}>
<KeyboardAvoidingView
enabled={!Platform.isPad}
behavior={Platform.OS === 'ios' ? 'position' : null}
keyboardShouldPersistTaps="handled"
>
<View style={styles.wrapper}>
<BlueCard style={styles.mainCard}>
<View style={[styles.input, stylesHooks.input]}>
<TextInput
style={styles.text}
maxHeight={100}
minHeight={100}
maxWidth="100%"
minWidth="100%"
multiline
editable
placeholder={loc.autofill_word.enter}
placeholderTextColor="#81868e"
value={mnemonic}
onChangeText={handleUpdateMnemonic}
testID="MnemonicInput"
/>
</View>

<BlueSpacing10 />
<BlueButton title={loc.send.input_clear} onPress={clearMnemonicInput} />
<BlueSpacing20 />
<BlueText style={styles.center} testID="Result">
{result}
</BlueText>
<BlueSpacing20 />
<View>
<BlueButton
disabled={mnemonic.trim().length === 0}
title={loc.autofill_word.generate_word}
onPress={checkMnemonic}
testID="GenerateWord"
/>
</View>
<BlueSpacing20 />
</BlueCard>
</View>
</KeyboardAvoidingView>
</SafeBlueArea>
);
};

export default GenerateWord;
GenerateWord.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.autofill_word.title }));

const styles = StyleSheet.create({
wrapper: {
marginTop: 16,
alignItems: 'center',
justifyContent: 'flex-start',
},
blueArea: {
paddingTop: 19,
},
mainCard: {
padding: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
},
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
alignItems: 'center',
borderRadius: 4,
},
center: {
textAlign: 'center',
},
text: {
padding: 8,
minHeight: 33,
color: '#81868e',
},
});
39 changes: 39 additions & 0 deletions tests/unit/checksumWords.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { generateChecksumWords } from '../../blue_modules/checksumWords';
import { validateMnemonic } from '../../blue_modules/bip39';
const assert = require('assert');

describe('generateChecksumWords', () => {
it('generates 128 valid words for an 11 word input', () => {
const input = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon';
const result = generateChecksumWords(input);
assert.ok(result);
assert.strictEqual(result.length, 128);

for (let i = 0; i < 128; i++) {
assert.ok(validateMnemonic(input + ' ' + result[i]));
}
});

it('generates 8 valid words for a 23 word input', () => {
const input =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon ' +
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon';
const result = generateChecksumWords(input);
assert.ok(result);
assert.strictEqual(result.length, 8);

for (let i = 0; i < 8; i++) {
assert.ok(validateMnemonic(input + ' ' + result[i]));
}
});

it('fails with an invalid partial phrase', () => {
const result = generateChecksumWords('lorem ipsum dolor sit amet');
assert.strictEqual(result, false);
});

it('fails with a completed phrase', () => {
const result = generateChecksumWords('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about');
assert.strictEqual(result, false);
});
});

0 comments on commit 53271d1

Please sign in to comment.