diff --git a/libloki/modules/mnemonic.d.ts b/libloki/modules/mnemonic.d.ts deleted file mode 100644 index 75c47462c..000000000 --- a/libloki/modules/mnemonic.d.ts +++ /dev/null @@ -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; - pubkey_to_secret_words(pubKey?: string): string; -} diff --git a/libloki/modules/mnemonic.js b/libloki/modules/mnemonic.js deleted file mode 100644 index fb81108d0..000000000 --- a/libloki/modules/mnemonic.js +++ /dev/null @@ -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(' '); -} diff --git a/package.json b/package.json index ff3427d1a..7c28ee5f3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/preload.js b/preload.js index 71b1f1e4d..74aedbe80 100644 --- a/preload.js +++ b/preload.js @@ -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 diff --git a/ts/components/session/SessionSeedModal.tsx b/ts/components/session/SessionSeedModal.tsx index 27f45d41c..9d60dec6c 100644 --- a/ts/components/session/SessionSeedModal.tsx +++ b/ts/components/session/SessionSeedModal.tsx @@ -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; diff --git a/ts/components/session/registration/RegistrationTabs.tsx b/ts/components/session/registration/RegistrationTabs.tsx index f59a6d710..ec7bd8500 100644 --- a/ts/components/session/registration/RegistrationTabs.tsx +++ b/ts/components/session/registration/RegistrationTabs.tsx @@ -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 { 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) { diff --git a/ts/session/crypto/mnemonic.ts b/ts/session/crypto/mnemonic.ts new file mode 100644 index 000000000..daf174b7d --- /dev/null +++ b/ts/session/crypto/mnemonic.ts @@ -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, 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; + 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; + } +>; +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 { + 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) + ); + } + } +} diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 704ba9e86..607b65152 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -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() { diff --git a/ts/window.d.ts b/ts/window.d.ts index 17556ca65..a8c48fd8c 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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; diff --git a/yarn.lock b/yarn.lock index 0727a0962..f4e984193 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"