move mnemonic.js to typescript

pull/1530/head
Audric Ackermann 4 years ago
parent 553f264d12
commit 35e57f2509
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -1,6 +0,0 @@
export interface RecoveryPhraseUtil {
mn_encode(str: string, wordset_name: string): string;
mn_decode(str: string, wordset_name: string): string;
get_languages(): Array<string>;
pubkey_to_secret_words(pubKey?: string): string;
}

@ -1,176 +0,0 @@
const crc32 = require('buffer-crc32');
module.exports = {
mn_encode,
mn_decode,
get_languages,
pubkey_to_secret_words,
};
class MnemonicError extends Error {}
/*
mnemonic.js : Converts between 4-byte aligned strings and a human-readable
sequence of words. Uses 1626 common words taken from wikipedia article:
http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry
Originally written in python special for Electrum (lightweight Bitcoin client).
This version has been reimplemented in javascript and placed in public domain.
*/
var mn_default_wordset = 'english';
function mn_get_checksum_index(words, prefix_len) {
var trimmed_words = '';
for (var i = 0; i < words.length; i++) {
trimmed_words += words[i].slice(0, prefix_len);
}
var checksum = crc32.unsigned(trimmed_words);
var index = checksum % words.length;
return index;
}
function mn_encode(str, wordset_name) {
'use strict';
wordset_name = wordset_name || mn_default_wordset;
var wordset = mn_words[wordset_name];
var out = [];
var n = wordset.words.length;
for (var j = 0; j < str.length; j += 8) {
str =
str.slice(0, j) +
mn_swap_endian_4byte(str.slice(j, j + 8)) +
str.slice(j + 8);
}
for (var i = 0; i < str.length; i += 8) {
var x = parseInt(str.substr(i, 8), 16);
var w1 = x % n;
var w2 = (Math.floor(x / n) + w1) % n;
var w3 = (Math.floor(Math.floor(x / n) / n) + w2) % n;
out = out.concat([wordset.words[w1], wordset.words[w2], wordset.words[w3]]);
}
if (wordset.prefix_len > 0) {
out.push(out[mn_get_checksum_index(out, wordset.prefix_len)]);
}
return out.join(' ');
}
function mn_swap_endian_4byte(str) {
'use strict';
if (str.length !== 8)
throw new MnemonicError('Invalid input length: ' + str.length);
return str.slice(6, 8) + str.slice(4, 6) + str.slice(2, 4) + str.slice(0, 2);
}
function mn_decode(str, wordset_name) {
'use strict';
wordset_name = wordset_name || mn_default_wordset;
var wordset = mn_words[wordset_name];
var out = '';
var n = wordset.words.length;
var wlist = str.split(' ');
var checksum_word = '';
if (wlist.length < 12)
throw new MnemonicError("You've entered too few words, please try again");
if (
(wordset.prefix_len === 0 && wlist.length % 3 !== 0) ||
(wordset.prefix_len > 0 && wlist.length % 3 === 2)
)
throw new MnemonicError("You've entered too few words, please try again");
if (wordset.prefix_len > 0 && wlist.length % 3 === 0)
throw new MnemonicError(
'You seem to be missing the last word in your private key, please try again'
);
if (wordset.prefix_len > 0) {
// Pop checksum from mnemonic
checksum_word = wlist.pop();
}
// Decode mnemonic
for (var i = 0; i < wlist.length; i += 3) {
var w1, w2, w3;
if (wordset.prefix_len === 0) {
w1 = wordset.words.indexOf(wlist[i]);
w2 = wordset.words.indexOf(wlist[i + 1]);
w3 = wordset.words.indexOf(wlist[i + 2]);
} else {
w1 = wordset.trunc_words.indexOf(wlist[i].slice(0, wordset.prefix_len));
w2 = wordset.trunc_words.indexOf(
wlist[i + 1].slice(0, wordset.prefix_len)
);
w3 = wordset.trunc_words.indexOf(
wlist[i + 2].slice(0, wordset.prefix_len)
);
}
if (w1 === -1 || w2 === -1 || w3 === -1) {
throw new MnemonicError('invalid word in mnemonic');
}
var x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n);
if (x % n != w1)
throw new MnemonicError(
'Something went wrong when decoding your private key, please try again'
);
out += mn_swap_endian_4byte(('0000000' + x.toString(16)).slice(-8));
}
// Verify checksum
if (wordset.prefix_len > 0) {
var index = mn_get_checksum_index(wlist, wordset.prefix_len);
var expected_checksum_word = wlist[index];
if (
expected_checksum_word.slice(0, wordset.prefix_len) !==
checksum_word.slice(0, wordset.prefix_len)
) {
throw new MnemonicError(
'Your private key could not be verified, please verify the checksum word'
);
}
}
return out;
}
// Note: the value is the prefix_len
const languages = {
chinese_simplified: 1,
dutch: 4,
electrum: 0,
english: 3,
esperanto: 4,
french: 4,
german: 4,
italian: 4,
japanese: 3,
lojban: 4,
portuguese: 4,
russian: 4,
spanish: 4,
};
let mn_words = {};
for (let [language, prefix_len] of Object.entries(languages)) {
mn_words[language] = {
prefix_len,
words: require(`../../mnemonic_languages/${language}`),
};
}
function get_languages() {
return Object.keys(mn_words);
}
for (var i in mn_words) {
if (mn_words.hasOwnProperty(i)) {
if (mn_words[i].prefix_len === 0) {
continue;
}
mn_words[i].trunc_words = [];
for (var j = 0; j < mn_words[i].words.length; ++j) {
mn_words[i].trunc_words.push(
mn_words[i].words[j].slice(0, mn_words[i].prefix_len)
);
}
}
}
function pubkey_to_secret_words(pubKey) {
return mn_encode(pubKey.slice(2), 'english')
.split(' ')
.slice(0, 3)
.join(' ');
}

@ -129,13 +129,9 @@
},
"devDependencies": {
"@types/backbone": "^1.4.2",
"@types/bytebuffer": "^5.0.41",
"@types/blueimp-load-image": "^2.23.8",
"@types/emoji-mart": "^2.11.3",
"@types/moment": "^2.13.0",
"@types/react-mentions": "^3.3.1",
"@types/react-mic": "^12.4.1",
"@types/styled-components": "^5.1.4",
"@types/buffer-crc32": "^0.2.0",
"@types/bytebuffer": "^5.0.41",
"@types/chai": "4.1.2",
"@types/chai-as-promised": "^7.1.2",
"@types/classnames": "2.2.3",
@ -143,6 +139,7 @@
"@types/config": "0.0.34",
"@types/dompurify": "^2.0.0",
"@types/electron-is-dev": "^1.1.1",
"@types/emoji-mart": "^2.11.3",
"@types/filesize": "3.6.0",
"@types/fs-extra": "5.0.5",
"@types/jquery": "3.3.29",
@ -151,12 +148,15 @@
"@types/lodash": "4.14.106",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.0.0",
"@types/moment": "^2.13.0",
"@types/node-fetch": "^2.5.7",
"@types/pify": "3.0.2",
"@types/qs": "6.5.1",
"@types/rc-slider": "^8.6.5",
"@types/react": "16.8.5",
"@types/react-dom": "16.8.2",
"@types/react-mentions": "^3.3.1",
"@types/react-mic": "^12.4.1",
"@types/react-portal": "^4.0.2",
"@types/react-redux": "7.1.9",
"@types/react-virtualized": "9.18.12",
@ -164,6 +164,7 @@
"@types/rimraf": "2.0.2",
"@types/semver": "5.5.0",
"@types/sinon": "9.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "3.4.4",
"arraybuffer-loader": "1.0.3",
"asar": "0.14.0",

@ -356,7 +356,6 @@ window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api');
window.LokiFileServerAPI = require('./js/modules/loki_file_server_api');
window.LokiPushNotificationServerApi = require('./js/modules/loki_push_notification_server_api');
window.mnemonic = require('./libloki/modules/mnemonic');
const WorkerInterface = require('./js/modules/util_worker_interface');
// A Worker with a 3 minute timeout

@ -7,6 +7,7 @@ import { DefaultTheme, withTheme } from 'styled-components';
import { PasswordUtil } from '../../util';
import { getPasswordHash } from '../../data/data';
import { QRCode } from 'react-qr-svg';
import { mn_decode } from '../../session/crypto/mnemonic';
interface Props {
onClose: any;

@ -21,6 +21,7 @@ import {
} from '../../../util/accountManager';
import { fromHex } from '../../../session/utils/String';
import { TaskTimedOutError } from '../../../session/utils/Promise';
import { mn_decode } from '../../../session/crypto/mnemonic';
export const MAX_USERNAME_LENGTH = 20;
// tslint:disable: use-simple-attributes
@ -304,10 +305,9 @@ export class RegistrationTabs extends React.Component<any, State> {
private async generateMnemonicAndKeyPair() {
if (this.state.generatedRecoveryPhrase === '') {
const language = 'english';
const mnemonic = await generateMnemonic(language);
const mnemonic = await generateMnemonic();
let seedHex = window.mnemonic.mn_decode(mnemonic, language);
let seedHex = mn_decode(mnemonic);
// handle shorter than 32 bytes seeds
const privKeyHexLength = 32 * 2;
if (seedHex.length !== privKeyHexLength) {

@ -0,0 +1,178 @@
import crc32 from 'buffer-crc32';
class MnemonicError extends Error {}
/*
mnemonic.js : Converts between 4-byte aligned strings and a human-readable
sequence of words. Uses 1626 common words taken from wikipedia article:
http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry
Originally written in python special for Electrum (lightweight Bitcoin client).
This version has been reimplemented in javascript and placed in public domain.
*/
const MN_DEFAULT_WORDSET = 'english';
function mn_get_checksum_index(words: Array<string>, prefixLen: number) {
let trimmedWords = '';
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < words.length; i++) {
trimmedWords += words[i].slice(0, prefixLen);
}
const checksum = crc32.unsigned(trimmedWords as any);
const index = checksum % words.length;
return index;
}
export function mn_encode(
str: string,
wordsetName: string = MN_DEFAULT_WORDSET
): string {
const wordset = mnWords[wordsetName];
let out = [] as Array<any>;
const n = wordset.words.length;
let strCopy = str;
for (let j = 0; j < strCopy.length; j += 8) {
strCopy =
strCopy.slice(0, j) +
mn_swap_endian_4byte(strCopy.slice(j, j + 8)) +
strCopy.slice(j + 8);
}
for (let i = 0; i < strCopy.length; i += 8) {
const x = parseInt(strCopy.substr(i, 8), 16);
const w1 = x % n;
const w2 = (Math.floor(x / n) + w1) % n;
const w3 = (Math.floor(Math.floor(x / n) / n) + w2) % n;
out = out.concat([wordset.words[w1], wordset.words[w2], wordset.words[w3]]);
}
if (wordset.prefixLen > 0) {
out.push(out[mn_get_checksum_index(out, wordset.prefixLen)]);
}
return out.join(' ');
}
function mn_swap_endian_4byte(str: string) {
if (str.length !== 8) {
throw new MnemonicError(`Invalid input length: ${str.length}`);
}
return str.slice(6, 8) + str.slice(4, 6) + str.slice(2, 4) + str.slice(0, 2);
}
export function mn_decode(
str: string,
wordsetName: string = MN_DEFAULT_WORDSET
): string {
const wordset = mnWords[wordsetName];
let out = '';
const n = wordset.words.length;
const wlist = str.split(' ');
let checksumWord = '';
if (wlist.length < 12) {
throw new MnemonicError("You've entered too few words, please try again");
}
if (
(wordset.prefixLen === 0 && wlist.length % 3 !== 0) ||
(wordset.prefixLen > 0 && wlist.length % 3 === 2)
) {
throw new MnemonicError("You've entered too few words, please try again");
}
if (wordset.prefixLen > 0 && wlist.length % 3 === 0) {
throw new MnemonicError(
'You seem to be missing the last word in your private key, please try again'
);
}
if (wordset.prefixLen > 0) {
// Pop checksum from mnemonic
checksumWord = wlist.pop() as string;
}
// Decode mnemonic
for (let i = 0; i < wlist.length; i += 3) {
// tslint:disable-next-line: one-variable-per-declaration
let w1, w2, w3;
if (wordset.prefixLen === 0) {
w1 = wordset.words.indexOf(wlist[i]);
w2 = wordset.words.indexOf(wlist[i + 1]);
w3 = wordset.words.indexOf(wlist[i + 2]);
} else {
w1 = wordset.truncWords.indexOf(wlist[i].slice(0, wordset.prefixLen));
w2 = wordset.truncWords.indexOf(wlist[i + 1].slice(0, wordset.prefixLen));
w3 = wordset.truncWords.indexOf(wlist[i + 2].slice(0, wordset.prefixLen));
}
if (w1 === -1 || w2 === -1 || w3 === -1) {
throw new MnemonicError('invalid word in mnemonic');
}
// tslint:disable-next-line: restrict-plus-operands
const x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n);
if (x % n !== w1) {
throw new MnemonicError(
'Something went wrong when decoding your private key, please try again'
);
}
out += mn_swap_endian_4byte(`0000000${x.toString(16)}`.slice(-8));
}
// Verify checksum
if (wordset.prefixLen > 0) {
const index = mn_get_checksum_index(wlist, wordset.prefixLen);
const expectedChecksumWord = wlist[index];
if (
expectedChecksumWord.slice(0, wordset.prefixLen) !==
checksumWord.slice(0, wordset.prefixLen)
) {
throw new MnemonicError(
'Your private key could not be verified, please verify the checksum word'
);
}
}
return out;
}
// Note: the value is the prefix_len
const languages = {
chinese_simplified: 1,
dutch: 4,
electrum: 0,
english: 3,
esperanto: 4,
french: 4,
german: 4,
italian: 4,
japanese: 3,
lojban: 4,
portuguese: 4,
russian: 4,
spanish: 4,
};
const mnWords = {} as Record<
string,
{
prefixLen: number;
words: any;
truncWords: Array<any>;
}
>;
for (const [language, prefixLen] of Object.entries(languages)) {
mnWords[language] = {
prefixLen,
// tslint:disable-next-line: non-literal-require
words: require(`../../../mnemonic_languages/${language}`),
truncWords: [],
};
}
export function get_languages(): Array<string> {
return Object.keys(mnWords);
}
// tslint:disable: prefer-for-of
// tslint:disable: no-for-in
for (const i in mnWords) {
if (mnWords.hasOwnProperty(i)) {
if (mnWords[i].prefixLen === 0) {
continue;
}
for (let j = 0; j < mnWords[i].words.length; ++j) {
mnWords[i].truncWords.push(
mnWords[i].words[j].slice(0, mnWords[i].prefixLen)
);
}
}
}

@ -17,6 +17,7 @@ import {
} from '../data/data';
import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils';
import { actions as userActions } from '../state/ducks/user';
import { mn_decode, mn_encode } from '../session/crypto/mnemonic';
/**
* Might throw
@ -49,7 +50,7 @@ export async function sessionGenerateKeyPair(
}
const generateKeypair = async (mnemonic: string, mnemonicLanguage: string) => {
let seedHex = window.mnemonic.mn_decode(mnemonic, mnemonicLanguage);
let seedHex = mn_decode(mnemonic, mnemonicLanguage);
// handle shorter than 32 bytes seeds
const privKeyHexLength = 32 * 2;
if (seedHex.length !== privKeyHexLength) {
@ -139,13 +140,13 @@ export async function registerSingleDevice(
await registrationDone(pubKeyString, profileName);
}
export async function generateMnemonic(language = 'english') {
export async function generateMnemonic() {
// Note: 4 bytes are converted into 3 seed words, so length 12 seed words
// (13 - 1 checksum) are generated using 12 * 4 / 3 = 16 bytes.
const seedSize = 16;
const seed = (await getSodium()).randombytes_buf(seedSize);
const hex = toHex(seed);
return window.mnemonic.mn_encode(hex, language);
return mn_encode(hex);
}
export async function clearSessionsAndPreKeys() {

2
ts/window.d.ts vendored

@ -9,7 +9,6 @@ import { LokiMessageInterface } from '../js/modules/loki_message_api';
import { SwarmPolling } from './session/snode_api/swarmPolling';
import { LibTextsecure } from '../libtextsecure';
import { RecoveryPhraseUtil } from '../libloki/modules/mnemonic';
import { ConfirmationDialogParams } from '../background';
import {} from 'styled-components/cssprop';
@ -67,7 +66,6 @@ declare global {
lokiMessageAPI: LokiMessageInterface;
lokiPublicChatAPI: LokiPublicChatFactoryInterface;
lokiSnodeAPI: LokiSnodeAPI;
mnemonic: RecoveryPhraseUtil;
onLogin: any;
resetDatabase: any;
restart: any;

@ -372,6 +372,13 @@
resolved "https://registry.yarnpkg.com/@types/blueimp-load-image/-/blueimp-load-image-2.23.8.tgz#0d10f12bf57f050aceac06dcc76390ae759c979a"
integrity sha512-dy98N4odO9L1zgo2a6yVHRRYUeYRJfl0hg3dcapyxqNq5KF8Zgz5lFWgDMOsMC06VAs0mnVKDRJE4+U/A5Km3A==
"@types/buffer-crc32@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@types/buffer-crc32/-/buffer-crc32-0.2.0.tgz#a4d94b1a4f01ea414268e9ef7576c7d32a1a14e4"
integrity sha512-6lBhJ55o2DKVCxTanyS6ohWRyebCcyivIK7pRHiwZuOYbUhivcByYBrvm2dc9f72LZP12mH5mGxUhG7JQ64lQg==
dependencies:
"@types/node" "*"
"@types/bytebuffer@^5.0.41":
version "5.0.41"
resolved "https://registry.yarnpkg.com/@types/bytebuffer/-/bytebuffer-5.0.41.tgz#6850dba4d4cd2846596b4842874d5bfc01cd3db1"

Loading…
Cancel
Save