feat: entropy refactor

This commit is contained in:
Ivan Vershigora 2024-06-01 14:48:22 +01:00
parent 720a47cbd0
commit 94d43e1855
No known key found for this signature in database
GPG key ID: DCCF7FB5ED2CEBD7
10 changed files with 275 additions and 183 deletions

View file

@ -159,9 +159,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
} }
async generateFromEntropy(user: Buffer) { async generateFromEntropy(user: Buffer) {
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0); if (user.length !== 32 && user.length !== 16) {
const buf = Buffer.concat([user, random], 32); throw new Error('Entropy has to be 16 or 32 bytes long');
this.secret = bip39.entropyToMnemonic(buf.toString('hex')); }
this.secret = bip39.entropyToMnemonic(user.toString('hex'));
} }
_getExternalWIFByIndex(index: number): string | false { _getExternalWIFByIndex(index: number): string | false {

View file

@ -63,18 +63,10 @@ export class LegacyWallet extends AbstractWallet {
} }
async generateFromEntropy(user: Buffer): Promise<void> { async generateFromEntropy(user: Buffer): Promise<void> {
let i = 0; if (user.length !== 32) {
do { throw new Error('Entropy should be 32 bytes');
i += 1;
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
try {
this.secret = ECPair.fromPrivateKey(buf).toWIF();
return;
} catch (e) {
if (i === 5) throw e;
} }
} while (true); this.secret = ECPair.fromPrivateKey(user).toWIF();
} }
getAddress(): string | false { getAddress(): string | false {

View file

@ -131,7 +131,7 @@ interface FButtonProps {
last?: boolean; last?: boolean;
disabled?: boolean; disabled?: boolean;
onPress: () => void; onPress: () => void;
onLongPress: () => void; onLongPress?: () => void;
} }
export const FButton = ({ text, icon, width, first, last, ...props }: FButtonProps) => { export const FButton = ({ text, icon, width, first, last, ...props }: FButtonProps) => {

View file

@ -43,7 +43,8 @@
"entropy": { "entropy": {
"save": "Save", "save": "Save",
"title": "Entropy", "title": "Entropy",
"undo": "Undo" "undo": "Undo",
"amountOfEntropy": "{bits} of {limit} bits"
}, },
"errors": { "errors": {
"broadcast": "Broadcast failed.", "broadcast": "Broadcast failed.",
@ -382,6 +383,8 @@
"add_bitcoin": "Bitcoin", "add_bitcoin": "Bitcoin",
"add_bitcoin_explain": "Simple and powerful Bitcoin wallet", "add_bitcoin_explain": "Simple and powerful Bitcoin wallet",
"add_create": "Create", "add_create": "Create",
"add_entropy": "Entropy",
"add_entropy_bytes": "{bytes} bytes of entropy",
"add_entropy_generated": "{gen} bytes of generated entropy", "add_entropy_generated": "{gen} bytes of generated entropy",
"add_entropy_provide": "Provide entropy via dice rolls", "add_entropy_provide": "Provide entropy via dice rolls",
"add_entropy_remain": "{gen} bytes of generated entropy. Remaining {rem} bytes will be obtained from the System random number generator.", "add_entropy_remain": "{gen} bytes of generated entropy. Remaining {rem} bytes will be obtained from the System random number generator.",
@ -395,6 +398,10 @@
"add_title": "Add Wallet", "add_title": "Add Wallet",
"add_wallet_name": "Name", "add_wallet_name": "Name",
"add_wallet_type": "Type", "add_wallet_type": "Type",
"add_wallet_seed_length": "Seed Length",
"add_wallet_seed_length_message": "Choose the length of the seed phrase you wish to use for this wallet.",
"add_wallet_seed_length_12": "12 words",
"add_wallet_seed_length_24": "24 words",
"clipboard_bitcoin": "You have a Bitcoin address on your clipboard. Would you like to use it for a transaction?", "clipboard_bitcoin": "You have a Bitcoin address on your clipboard. Would you like to use it for a transaction?",
"clipboard_lightning": "You have a Lightning invoice on your clipboard. Would you like to use it for a transaction?", "clipboard_lightning": "You have a Lightning invoice on your clipboard. Would you like to use it for a transaction?",
"details_address": "Address", "details_address": "Address",

View file

@ -11,7 +11,7 @@ const ImportWallet = lazy(() => import('../screen/wallets/import'));
const PleaseBackup = lazy(() => import('../screen/wallets/PleaseBackup')); const PleaseBackup = lazy(() => import('../screen/wallets/PleaseBackup'));
const PleaseBackupLNDHub = lazy(() => import('../screen/wallets/pleaseBackupLNDHub')); const PleaseBackupLNDHub = lazy(() => import('../screen/wallets/pleaseBackupLNDHub'));
const PleaseBackupLdk = lazy(() => import('../screen/wallets/pleaseBackupLdk')); const PleaseBackupLdk = lazy(() => import('../screen/wallets/pleaseBackupLdk'));
const ProvideEntropy = lazy(() => import('../screen/wallets/provideEntropy')); const ProvideEntropy = lazy(() => import('../screen/wallets/ProvideEntropy'));
const WalletsAddMultisig = lazy(() => import('../screen/wallets/addMultisig')); const WalletsAddMultisig = lazy(() => import('../screen/wallets/addMultisig'));
const WalletsAddMultisigStep2 = lazy(() => import('../screen/wallets/addMultisigStep2')); const WalletsAddMultisigStep2 = lazy(() => import('../screen/wallets/addMultisigStep2'));
const WalletsAddMultisigHelp = lazy(() => import('../screen/wallets/addMultisigHelp')); const WalletsAddMultisigHelp = lazy(() => import('../screen/wallets/addMultisigHelp'));

View file

@ -3,6 +3,7 @@ import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useReducer } from 'react'; import React, { useEffect, useReducer } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Alert,
Keyboard, Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
LayoutAnimation, LayoutAnimation,
@ -53,7 +54,7 @@ interface State {
label: string; label: string;
selectedWalletType: ButtonSelected; selectedWalletType: ButtonSelected;
backdoorPressed: number; backdoorPressed: number;
entropy: string | any[] | undefined; entropy: Buffer | undefined;
entropyButtonText: string; entropyButtonText: string;
} }
@ -161,18 +162,13 @@ const WalletsAdd: React.FC = () => {
}); });
}, [colorScheme, setOptions]); }, [colorScheme, setOptions]);
const entropyGenerated = (newEntropy: string | any[]) => { const entropyGenerated = (newEntropy: Buffer) => {
let entropyTitle; let entropyTitle;
if (!newEntropy) { if (!newEntropy) {
entropyTitle = loc.wallets.add_entropy_provide; entropyTitle = loc.wallets.add_entropy_provide;
} else if (newEntropy.length < 32) {
entropyTitle = loc.formatString(loc.wallets.add_entropy_remain, {
gen: newEntropy.length,
rem: 32 - newEntropy.length,
});
} else { } else {
entropyTitle = loc.formatString(loc.wallets.add_entropy_generated, { entropyTitle = loc.formatString(loc.wallets.add_entropy_bytes, {
gen: newEntropy.length, bytes: newEntropy.length,
}); });
} }
setEntropy(newEntropy); setEntropy(newEntropy);
@ -203,7 +199,7 @@ const WalletsAdd: React.FC = () => {
dispatch({ type: 'INCREMENT_BACKDOOR_PRESSED', payload: value }); dispatch({ type: 'INCREMENT_BACKDOOR_PRESSED', payload: value });
}; };
const setEntropy = (value: string | any[]) => { const setEntropy = (value: Buffer) => {
dispatch({ type: 'SET_ENTROPY', payload: value }); dispatch({ type: 'SET_ENTROPY', payload: value });
}; };
@ -236,7 +232,6 @@ const WalletsAdd: React.FC = () => {
if (selectedWalletType === ButtonSelected.ONCHAIN) { if (selectedWalletType === ButtonSelected.ONCHAIN) {
if (entropy) { if (entropy) {
try { try {
// @ts-ignore: Return later to update
await w.generateFromEntropy(entropy); await w.generateFromEntropy(entropy);
} catch (e: any) { } catch (e: any) {
console.log(e.toString()); console.log(e.toString());
@ -334,8 +329,34 @@ const WalletsAdd: React.FC = () => {
}; };
const navigateToEntropy = () => { const navigateToEntropy = () => {
Alert.alert(
loc.wallets.add_wallet_seed_length,
loc.wallets.add_wallet_seed_length_message,
[
{
text: loc._.cancel,
onPress: () => {},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_12,
onPress: () => {
// @ts-ignore: Return later to update // @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated }); navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 12 });
},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_24,
onPress: () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 24 });
},
style: 'default',
},
],
{ cancelable: true },
);
}; };
const navigateToImportWallet = () => { const navigateToImportWallet = () => {

View file

@ -1,8 +1,18 @@
import { useNavigation, useRoute } from '@react-navigation/native'; import { useNavigation, useRoute } from '@react-navigation/native';
import BN from 'bignumber.js'; import BN from 'bignumber.js';
import PropTypes from 'prop-types'; import React, { useEffect, useReducer, useState } from 'react';
import React, { useReducer, useState } from 'react'; import {
import { Dimensions, Image, PixelRatio, ScrollView, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'; Alert,
Dimensions,
Image,
PixelRatio,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import { Icon } from 'react-native-elements'; import { Icon } from 'react-native-elements';
import { BlueSpacing20 } from '../../BlueComponents'; import { BlueSpacing20 } from '../../BlueComponents';
@ -11,48 +21,81 @@ import SafeArea from '../../components/SafeArea';
import { Tabs } from '../../components/Tabs'; import { Tabs } from '../../components/Tabs';
import { BlueCurrentTheme, useTheme } from '../../components/themes'; import { BlueCurrentTheme, useTheme } from '../../components/themes';
import loc from '../../loc'; import loc from '../../loc';
import { randomBytes } from '../../class/rng';
const ENTROPY_LIMIT = 256; export enum EActionType {
push = 'push',
pop = 'pop',
limit = 'limit',
noop = 'noop',
}
const shiftLeft = (value, places) => value.multipliedBy(2 ** places); type TEntropy = { value: number; bits: number };
const shiftRight = (value, places) => value.div(2 ** places).dp(0, BN.ROUND_DOWN); type TState = { entropy: BN; bits: number; items: number[]; limit: number };
type TAction =
| ({ type: EActionType.push } & TEntropy)
| { type: EActionType.pop }
| { type: EActionType.limit; limit: number }
| { type: EActionType.noop };
type TPush = (v: TEntropy | null) => void;
type TPop = () => void;
const initialState = { entropy: BN(0), bits: 0, items: [] }; const initialState: TState = {
export const eReducer = (state = initialState, action) => { entropy: BN(0),
bits: 0,
items: [],
limit: 256,
};
const shiftLeft = (value: BN, places: number) => value.multipliedBy(2 ** places);
const shiftRight = (value: BN, places: number) => value.div(2 ** places).dp(0, BN.ROUND_DOWN);
export const eReducer = (state: TState = initialState, action: TAction): TState => {
switch (action.type) { switch (action.type) {
case 'push': { case EActionType.noop:
let { value, bits } = action; return state;
case EActionType.push: {
let value: number | BN = action.value;
let bits: number = action.bits;
if (value >= 2 ** bits) { if (value >= 2 ** bits) {
throw new TypeError("Can't push value exceeding size in bits"); throw new TypeError("Can't push value exceeding size in bits");
} }
if (state.bits === ENTROPY_LIMIT) return state; if (state.bits === state.limit) return state;
if (state.bits + bits > ENTROPY_LIMIT) { if (state.bits + bits > state.limit) {
value = shiftRight(BN(value), bits + state.bits - ENTROPY_LIMIT); value = shiftRight(BN(value), bits + state.bits - state.limit);
bits = ENTROPY_LIMIT - state.bits; bits = state.limit - state.bits;
} }
const entropy = shiftLeft(state.entropy, bits).plus(value); const entropy = shiftLeft(state.entropy, bits).plus(value);
const items = [...state.items, bits]; const items = [...state.items, bits];
return { entropy, bits: state.bits + bits, items }; return { ...state, entropy, bits: state.bits + bits, items };
} }
case 'pop': {
case EActionType.pop: {
if (state.bits === 0) return state; if (state.bits === 0) return state;
const bits = state.items.pop(); const bits = state.items.pop()!;
const entropy = shiftRight(state.entropy, bits); const entropy = shiftRight(state.entropy, bits);
return { entropy, bits: state.bits - bits, items: [...state.items] }; return { ...state, entropy, bits: state.bits - bits, items: [...state.items] };
} }
case EActionType.limit: {
return { ...state, limit: action.limit };
}
default: default:
return state; return state;
} }
}; };
export const entropyToHex = ({ entropy, bits }) => { export const entropyToHex = ({ entropy, bits }: { entropy: BN; bits: number }): string => {
if (bits === 0) return '0x'; if (bits === 0) return '0x';
const hex = entropy.toString(16); const hex = entropy.toString(16);
const hexSize = Math.floor((bits - 1) / 4) + 1; const hexSize = Math.floor((bits - 1) / 4) + 1;
return '0x' + '0'.repeat(hexSize - hex.length) + hex; return '0x' + '0'.repeat(hexSize - hex.length) + hex;
}; };
export const getEntropy = (number, base) => { export const getEntropy = (number: number, base: number): TEntropy | null => {
if (base === 1) return null; if (base === 1) return null;
let maxPow = 1; let maxPow = 1;
while (2 ** (maxPow + 1) <= base) { while (2 ** (maxPow + 1) <= base) {
@ -77,12 +120,12 @@ export const getEntropy = (number, base) => {
}; };
// cut entropy to bytes, convert to Buffer // cut entropy to bytes, convert to Buffer
export const convertToBuffer = ({ entropy, bits }) => { export const convertToBuffer = ({ entropy, bits }: { entropy: BN; bits: number }): Buffer => {
if (bits < 8) return Buffer.from([]); if (bits < 8) return Buffer.from([]);
const bytes = Math.floor(bits / 8); const bytes = Math.floor(bits / 8);
// convert to byte array // convert to byte array
let arr = []; const arr: string[] = [];
const ent = entropy.toString(16).split('').reverse(); const ent = entropy.toString(16).split('').reverse();
ent.forEach((v, index) => { ent.forEach((v, index) => {
if (index % 2 === 1) { if (index % 2 === 1) {
@ -91,18 +134,19 @@ export const convertToBuffer = ({ entropy, bits }) => {
arr.unshift(v); arr.unshift(v);
} }
}); });
arr = arr.map(i => parseInt(i, 16));
let arr2 = arr.map(i => parseInt(i, 16));
if (arr.length > bytes) { if (arr.length > bytes) {
arr.shift(); arr2.shift();
} else if (arr.length < bytes) { } else if (arr2.length < bytes) {
const zeros = [...Array(bytes - arr.length)].map(() => 0); const zeros = [...Array(bytes - arr2.length)].map(() => 0);
arr = [...zeros, ...arr]; arr2 = [...zeros, ...arr2];
} }
return Buffer.from(arr); return Buffer.from(arr2);
}; };
const Coin = ({ push }) => ( const Coin = ({ push }: { push: TPush }) => (
<View style={styles.coinRoot}> <View style={styles.coinRoot}>
<TouchableOpacity accessibilityRole="button" onPress={() => push(getEntropy(0, 2))} style={styles.coinBody}> <TouchableOpacity accessibilityRole="button" onPress={() => push(getEntropy(0, 2))} style={styles.coinBody}>
<Image style={styles.coinImage} source={require('../../img/coin1.png')} /> <Image style={styles.coinImage} source={require('../../img/coin1.png')} />
@ -113,26 +157,7 @@ const Coin = ({ push }) => (
</View> </View>
); );
Coin.propTypes = { const diceIcon = (i: number): string => {
push: PropTypes.func.isRequired,
};
const Dice = ({ push, sides }) => {
const { width } = useWindowDimensions();
const { colors } = useTheme();
const diceWidth = width / 4;
const stylesHook = StyleSheet.create({
dice: {
borderColor: colors.buttonBackgroundColor,
},
diceText: {
color: colors.foregroundColor,
},
diceContainer: {
backgroundColor: colors.elevated,
},
});
const diceIcon = i => {
switch (i) { switch (i) {
case 1: case 1:
return 'dice-one'; return 'dice-one';
@ -147,7 +172,23 @@ const Dice = ({ push, sides }) => {
default: default:
return 'dice-six'; return 'dice-six';
} }
}; };
const Dice = ({ push, sides }: { push: TPush; sides: number }) => {
const { width } = useWindowDimensions();
const { colors } = useTheme();
const diceWidth = width / 4;
const stylesHook = StyleSheet.create({
dice: {
borderColor: colors.buttonBackgroundColor,
},
diceText: {
color: colors.foregroundColor,
},
diceContainer: {
backgroundColor: colors.elevated,
},
});
return ( return (
<ScrollView contentContainerStyle={[styles.diceContainer, stylesHook.diceContainer]}> <ScrollView contentContainerStyle={[styles.diceContainer, stylesHook.diceContainer]}>
@ -168,48 +209,53 @@ const Dice = ({ push, sides }) => {
); );
}; };
Dice.propTypes = {
sides: PropTypes.number.isRequired,
push: PropTypes.func.isRequired,
};
const buttonFontSize = const buttonFontSize =
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22 PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
? 22 ? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26); : PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
const Buttons = ({ pop, save, colors }) => ( const Buttons = ({ pop, save, colors }: { pop: TPop; save: () => void; colors: any }) => (
<FContainer> <FContainer>
<FButton <FButton
onPress={pop} onPress={pop}
text={loc.entropy.undo}
icon={ icon={
<View style={styles.buttonsIcon}> <View style={styles.buttonsIcon}>
<Icon name="undo" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} /> <Icon name="undo" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View> </View>
} }
text={loc.entropy.undo}
/> />
<FButton <FButton
onPress={save} onPress={save}
text={loc.entropy.save}
icon={ icon={
<View style={styles.buttonsIcon}> <View style={styles.buttonsIcon}>
<Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} /> <Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View> </View>
} }
text={loc.entropy.save}
/> />
</FContainer> </FContainer>
); );
Buttons.propTypes = { const TollTab = ({ active }: { active: boolean }) => {
pop: PropTypes.func.isRequired, const { colors } = useTheme();
save: PropTypes.func.isRequired, return <Icon name="toll" type="material" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />;
colors: PropTypes.shape.isRequired, };
const D6Tab = ({ active }: { active: boolean }) => {
const { colors } = useTheme();
return <Icon name="dice" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />;
};
const D20Tab = ({ active }: { active: boolean }) => {
const { colors } = useTheme();
return <Icon name="dice-d20" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />;
}; };
const Entropy = () => { const Entropy = () => {
const [entropy, dispatch] = useReducer(eReducer, initialState); const [entropy, dispatch] = useReducer(eReducer, initialState);
const { onGenerated } = useRoute().params; // @ts-ignore: navigation is not typed yet
const { onGenerated, words } = useRoute().params;
const navigation = useNavigation(); const navigation = useNavigation();
const [tab, setTab] = useState(1); const [tab, setTab] = useState(1);
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
@ -223,12 +269,65 @@ const Entropy = () => {
}, },
}); });
const push = v => v && dispatch({ type: 'push', value: v.value, bits: v.bits }); useEffect(() => {
const pop = () => dispatch({ type: 'pop' }); dispatch({ type: EActionType.limit, limit: words === 24 ? 256 : 128 });
const save = () => { }, [dispatch, words]);
const handlePush: TPush = v => {
if (v === null) {
dispatch({ type: EActionType.noop });
} else {
dispatch({ type: EActionType.push, value: v.value, bits: v.bits });
}
};
const handlePop: TPop = () => {
dispatch({ type: EActionType.pop });
};
const handleSave = async () => {
let buf = convertToBuffer(entropy);
const bufLength = words === 24 ? 32 : 16;
const remaining = bufLength - buf.length;
let entropyTitle = '';
if (buf.length < bufLength) {
entropyTitle = loc.formatString(loc.wallets.add_entropy_remain, {
gen: buf.length,
rem: remaining,
});
} else {
entropyTitle = loc.formatString(loc.wallets.add_entropy_generated, {
gen: buf.length,
});
}
Alert.alert(
loc.wallets.add_entropy,
entropyTitle,
[
{
text: loc._.ok,
onPress: async () => {
if (remaining > 0) {
const random = await randomBytes(remaining);
buf = Buffer.concat([buf, random], bufLength);
}
// @ts-ignore: navigation is not typed yet
navigation.pop(); navigation.pop();
const buf = convertToBuffer(entropy);
onGenerated(buf); onGenerated(buf);
},
style: 'default',
},
{
text: loc._.cancel,
onPress: () => {},
style: 'default',
},
],
{ cancelable: true },
);
}; };
const hex = entropyToHex(entropy); const hex = entropyToHex(entropy);
@ -240,34 +339,19 @@ const Entropy = () => {
<BlueSpacing20 /> <BlueSpacing20 />
<TouchableOpacity accessibilityRole="button" onPress={() => setShow(!show)}> <TouchableOpacity accessibilityRole="button" onPress={() => setShow(!show)}>
<View style={[styles.entropy, stylesHook.entropy]}> <View style={[styles.entropy, stylesHook.entropy]}>
<Text style={[styles.entropyText, stylesHook.entropyText]}>{show ? hex : `${bits} of 256 bits`}</Text> <Text style={[styles.entropyText, stylesHook.entropyText]}>
{show ? hex : loc.formatString(loc.entropy.amountOfEntropy, { bits, limit: entropy.limit })}
</Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<Tabs <Tabs active={tab} onSwitch={setTab} tabs={[TollTab, D6Tab, D20Tab]} />
active={tab}
onSwitch={setTab}
tabs={[
// eslint-disable-next-line react/no-unstable-nested-components
({ active }) => (
<Icon name="toll" type="material" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />
),
// eslint-disable-next-line react/no-unstable-nested-components
({ active }) => (
<Icon name="dice" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />
),
// eslint-disable-next-line react/no-unstable-nested-components
({ active }) => (
<Icon name="dice-d20" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />
),
]}
/>
{tab === 0 && <Coin push={push} />} {tab === 0 && <Coin push={handlePush} />}
{tab === 1 && <Dice sides={6} push={push} />} {tab === 1 && <Dice sides={6} push={handlePush} />}
{tab === 2 && <Dice sides={20} push={push} />} {tab === 2 && <Dice sides={20} push={handlePush} />}
<Buttons pop={pop} save={save} colors={colors} /> <Buttons pop={handlePop} save={handleSave} colors={colors} />
</SafeArea> </SafeArea>
); );
}; };

View file

@ -140,31 +140,26 @@ describe('P2SH Segwit HD (BIP49)', () => {
}); });
it('can consume user generated entropy', async () => { it('can consume user generated entropy', async () => {
const hd = new HDSegwitP2SHWallet(); const hd1 = new HDSegwitP2SHWallet();
const zeroes = [...Array(32)].map(() => 0); const zeroes16 = [...Array(16)].map(() => 0);
await hd.generateFromEntropy(Buffer.from(zeroes)); await hd1.generateFromEntropy(Buffer.from(zeroes16));
assert.strictEqual(hd1.getSecret(), 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about');
const hd2 = new HDSegwitP2SHWallet();
const zeroes32 = [...Array(32)].map(() => 0);
await hd2.generateFromEntropy(Buffer.from(zeroes32));
assert.strictEqual( assert.strictEqual(
hd.getSecret(), hd2.getSecret(),
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art', 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art',
); );
}); });
it('can fullfill user generated entropy if less than 32 bytes provided', async () => { it('throws an error if not 32 bytes provided', async () => {
const hd = new HDSegwitP2SHWallet(); const hd = new HDSegwitP2SHWallet();
const zeroes = [...Array(16)].map(() => 0); const values = [...Array(31)].map(() => 1);
await hd.generateFromEntropy(Buffer.from(zeroes)); await assert.rejects(async () => await hd.generateFromEntropy(Buffer.from(values)), {
const secret = hd.getSecret(); message: 'Entropy has to be 16 or 32 bytes long',
assert.strictEqual(secret.startsWith('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon'), true); });
const secretWithoutChecksum = secret.split(' ');
secretWithoutChecksum.pop();
const secretWithoutChecksumString = secretWithoutChecksum.join(' ');
assert.strictEqual(
secretWithoutChecksumString.endsWith('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon'),
false,
);
assert.ok(secret.split(' ').length === 12 || secret.split(' ').length === 24);
}); });
it('can sign and verify messages', async () => { it('can sign and verify messages', async () => {

View file

@ -1,10 +1,7 @@
import assert from 'assert'; import assert from 'assert';
import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoin from 'bitcoinjs-lib';
import { ECPairFactory } from 'ecpair';
import ecc from '../../blue_modules/noble_ecc';
import { LegacyWallet } from '../../class'; import { LegacyWallet } from '../../class';
const ECPair = ECPairFactory(ecc);
describe('Legacy wallet', () => { describe('Legacy wallet', () => {
it('can validate addresses', () => { it('can validate addresses', () => {
@ -146,17 +143,12 @@ describe('Legacy wallet', () => {
assert.strictEqual(l.getSecret(), 'KwFfNUhSDaASSAwtG7ssQM1uVX8RgX5GHWnnLfhfiQDigjioWXHH'); assert.strictEqual(l.getSecret(), 'KwFfNUhSDaASSAwtG7ssQM1uVX8RgX5GHWnnLfhfiQDigjioWXHH');
}); });
it('can fullfill user generated entropy if less than 32 bytes provided', async () => { it('throws an error if not 32 bytes provided', async () => {
const l = new LegacyWallet(); const l = new LegacyWallet();
const values = [...Array(16)].map(() => 1); const values = [...Array(31)].map(() => 1);
await l.generateFromEntropy(Buffer.from(values)); await assert.rejects(async () => await l.generateFromEntropy(Buffer.from(values)), {
assert.strictEqual(l.getSecret().startsWith('KwFfNUhSDaASSAwtG7ssQM'), true); message: 'Entropy should be 32 bytes',
assert.strictEqual(l.getSecret().endsWith('GHWnnLfhfiQDigjioWXHH'), false); });
const keyPair = ECPair.fromWIF(l.getSecret());
assert.strictEqual(keyPair.privateKey.toString('hex').startsWith('01010101'), true);
assert.strictEqual(keyPair.privateKey.toString('hex').endsWith('01010101'), false);
assert.strictEqual(keyPair.privateKey.toString('hex').endsWith('00000000'), false);
assert.strictEqual(keyPair.privateKey.toString('hex').endsWith('ffffffff'), false);
}); });
it('can sign and verify messages', async () => { it('can sign and verify messages', async () => {

View file

@ -1,69 +1,69 @@
import assert from 'assert'; import assert from 'assert';
import { convertToBuffer, entropyToHex, eReducer, getEntropy } from '../../screen/wallets/provideEntropy'; import { convertToBuffer, EActionType, entropyToHex, eReducer, getEntropy } from '../../screen/wallets/ProvideEntropy';
describe('Entropy reducer and format', () => { describe('Entropy reducer and format', () => {
it('handles push and pop correctly', () => { it('handles push and pop correctly', () => {
let state = eReducer(undefined, { type: null }); let state = eReducer(undefined, { type: EActionType.noop });
assert.equal(entropyToHex(state), '0x'); assert.equal(entropyToHex(state), '0x');
state = eReducer(state, { type: 'push', value: 0, bits: 1 }); state = eReducer(state, { type: EActionType.push, value: 0, bits: 1 });
assert.equal(entropyToHex(state), '0x0'); assert.equal(entropyToHex(state), '0x0');
state = eReducer(state, { type: 'push', value: 0, bits: 1 }); state = eReducer(state, { type: EActionType.push, value: 0, bits: 1 });
assert.equal(entropyToHex(state), '0x0'); assert.equal(entropyToHex(state), '0x0');
state = eReducer(state, { type: 'push', value: 0, bits: 3 }); state = eReducer(state, { type: EActionType.push, value: 0, bits: 3 });
assert.equal(entropyToHex(state), '0x00'); assert.equal(entropyToHex(state), '0x00');
state = eReducer(state, { type: 'pop' }); state = eReducer(state, { type: EActionType.pop });
assert.equal(entropyToHex(state), '0x0'); assert.equal(entropyToHex(state), '0x0');
state = eReducer(state, { type: 'pop' }); state = eReducer(state, { type: EActionType.pop });
state = eReducer(state, { type: 'pop' }); state = eReducer(state, { type: EActionType.pop });
assert.equal(entropyToHex(state), '0x'); assert.equal(entropyToHex(state), '0x');
state = eReducer(state, { type: 'push', value: 1, bits: 1 }); state = eReducer(state, { type: EActionType.push, value: 1, bits: 1 });
assert.equal(entropyToHex(state), '0x1'); // 0b1 assert.equal(entropyToHex(state), '0x1'); // 0b1
state = eReducer(state, { type: 'push', value: 0, bits: 1 }); state = eReducer(state, { type: EActionType.push, value: 0, bits: 1 });
assert.equal(entropyToHex(state), '0x2'); // 0b10 assert.equal(entropyToHex(state), '0x2'); // 0b10
state = eReducer(state, { type: 'push', value: 0b01, bits: 2 }); state = eReducer(state, { type: EActionType.push, value: 0b01, bits: 2 });
assert.equal(entropyToHex(state), '0x9'); // 0b1001 assert.equal(entropyToHex(state), '0x9'); // 0b1001
state = eReducer(state, { type: 'push', value: 0b10, bits: 2 }); state = eReducer(state, { type: EActionType.push, value: 0b10, bits: 2 });
assert.equal(entropyToHex(state), '0x26'); // 0b100110 assert.equal(entropyToHex(state), '0x26'); // 0b100110
}); });
it('handles 128 bits correctly', () => { it('handles 128 bits correctly', () => {
const state = eReducer(undefined, { type: 'push', value: 0, bits: 128 }); const state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 128 });
assert.equal(entropyToHex(state), '0x00000000000000000000000000000000'); assert.equal(entropyToHex(state), '0x00000000000000000000000000000000');
}); });
it('handles 256 bits correctly', () => { it('handles 256 bits correctly', () => {
let state = eReducer(undefined, { type: null }); // get init state let state = eReducer(undefined, { type: EActionType.noop }); // get init state
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const i of [...Array(256)]) { for (const i of [...Array(256)]) {
state = eReducer(state, { type: 'push', value: 1, bits: 1 }); state = eReducer(state, { type: EActionType.push, value: 1, bits: 1 });
} }
assert.equal(entropyToHex(state), '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); assert.equal(entropyToHex(state), '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
}); });
it('handles pop when empty without error', () => { it('handles pop when empty without error', () => {
const state = eReducer(undefined, { type: 'pop' }); const state = eReducer(undefined, { type: EActionType.pop });
assert.equal(entropyToHex(state), '0x'); assert.equal(entropyToHex(state), '0x');
}); });
it('handles 256 bits limit', () => { it('handles 256 bits limit', () => {
let state = eReducer(undefined, { type: 'push', value: 0, bits: 254 }); let state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 254 });
state = eReducer(state, { type: 'push', value: 0b101, bits: 3 }); state = eReducer(state, { type: EActionType.push, value: 0b101, bits: 3 });
assert.equal(entropyToHex(state), '0x0000000000000000000000000000000000000000000000000000000000000002'); assert.equal(entropyToHex(state), '0x0000000000000000000000000000000000000000000000000000000000000002');
}); });
it('Throws error if you try to push value exceeding size in bits', () => { it('Throws error if you try to push value exceeding size in bits', () => {
assert.throws(() => eReducer(undefined, { type: 'push', value: 8, bits: 3 }), { assert.throws(() => eReducer(undefined, { type: EActionType.push, value: 8, bits: 3 }), {
name: 'TypeError', name: 'TypeError',
message: "Can't push value exceeding size in bits", message: "Can't push value exceeding size in bits",
}); });
@ -104,48 +104,48 @@ describe('getEntropy function', () => {
describe('convertToBuffer function', () => { describe('convertToBuffer function', () => {
it('zero bits', () => { it('zero bits', () => {
const state = eReducer(undefined, { type: null }); const state = eReducer(undefined, { type: EActionType.noop });
assert.deepEqual(convertToBuffer(state), Buffer.from([])); assert.deepEqual(convertToBuffer(state), Buffer.from([]));
}); });
it('8 zero bits', () => { it('8 zero bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0, bits: 8 }); const state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 8 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0])); assert.deepEqual(convertToBuffer(state), Buffer.from([0]));
}); });
it('8 filled bits', () => { it('8 filled bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0b11111111, bits: 8 }); const state = eReducer(undefined, { type: EActionType.push, value: 0b11111111, bits: 8 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0b11111111])); assert.deepEqual(convertToBuffer(state), Buffer.from([0b11111111]));
}); });
it('9 zero bits', () => { it('9 zero bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0, bits: 9 }); const state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 9 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0])); assert.deepEqual(convertToBuffer(state), Buffer.from([0]));
}); });
it('9 filled bits', () => { it('9 filled bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0b111111111, bits: 9 }); const state = eReducer(undefined, { type: EActionType.push, value: 0b111111111, bits: 9 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0b11111111])); assert.deepEqual(convertToBuffer(state), Buffer.from([0b11111111]));
}); });
it('9 bits', () => { it('9 bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0b111100111, bits: 9 }); const state = eReducer(undefined, { type: EActionType.push, value: 0b111100111, bits: 9 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0b11100111])); assert.deepEqual(convertToBuffer(state), Buffer.from([0b11100111]));
}); });
it('3 bytes', () => { it('3 bytes', () => {
let state = eReducer(undefined, { type: 'push', value: 1, bits: 8 }); let state = eReducer(undefined, { type: EActionType.push, value: 1, bits: 8 });
state = eReducer(state, { type: 'push', value: 2, bits: 8 }); state = eReducer(state, { type: EActionType.push, value: 2, bits: 8 });
state = eReducer(state, { type: 'push', value: 3, bits: 8 }); state = eReducer(state, { type: EActionType.push, value: 3, bits: 8 });
assert.deepEqual(convertToBuffer(state), Buffer.from([1, 2, 3])); assert.deepEqual(convertToBuffer(state), Buffer.from([1, 2, 3]));
}); });
it('256 bits or 32bytes', () => { it('256 bits or 32bytes', () => {
let state = eReducer(undefined, { type: null }); // get init state let state = eReducer(undefined, { type: EActionType.noop }); // get init state
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const i of [...Array(256)]) { for (const i of [...Array(256)]) {
state = eReducer(state, { type: 'push', value: 1, bits: 1 }); state = eReducer(state, { type: EActionType.push, value: 1, bits: 1 });
} }
const bytes = [...Array(32)].map(() => 255); const bytes = [...Array(32)].map(() => 255);