You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/session/crypto/mnemonic.ts

203 lines
6.3 KiB
TypeScript

import crc32 from 'buffer-crc32';
class MnemonicError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name; // Set the error name to the class name
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
// restore prototype chain
Object.setPrototypeOf(this, MnemonicError.prototype);
}
}
export class NotEnoughWordsError extends MnemonicError {
constructor(message = "You've entered too few words, please try again") {
super(message);
// restore prototype chain
Object.setPrototypeOf(this, NotEnoughWordsError.prototype);
}
}
/** NOTE 2024-08-07 (Will) Maximum number of words is 13 */
export class TooManyWordsError extends MnemonicError {
constructor(message = "You've entered too many words, please try again") {
super(message);
// restore prototype chain
Object.setPrototypeOf(this, TooManyWordsError.prototype);
}
}
export class InvalidWordsError extends MnemonicError {
constructor(message = "You've entered too few words, please try again") {
super(message);
// restore prototype chain
Object.setPrototypeOf(this, InvalidWordsError.prototype);
}
}
export class DecodingError extends MnemonicError {
constructor(message = 'Something went wrong when decoding your private key, please try again') {
super(message);
// restore prototype chain
Object.setPrototypeOf(this, DecodingError.prototype);
}
}
export class VerificationError extends MnemonicError {
constructor(message = 'Your private key could not be verified, please verify the checksum word') {
super(message);
// restore prototype chain
Object.setPrototypeOf(this, VerificationError.prototype);
}
}
/*
mnemonic.ts : 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 and has further been converted to TypeScript as part of the Session project.
*/
const MN_DEFAULT_WORDSET = 'english';
function mn_get_checksum_index(words: Array<string>, prefixLen: number) {
let trimmedWords = '';
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 mnEncode(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 mnDecode(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 NotEnoughWordsError();
}
if (wlist.length > 13) {
throw new TooManyWordsError();
}
if (
(wordset.prefixLen === 0 && wlist.length % 3 !== 0) ||
(wordset.prefixLen > 0 && wlist.length % 3 === 2)
) {
throw new NotEnoughWordsError();
}
if (wordset.prefixLen > 0 && wlist.length % 3 === 0) {
window.log.error(
'mnDecode(): You seem to be missing the last word in your private key, please try again'
);
throw new NotEnoughWordsError();
}
if (wordset.prefixLen > 0) {
// Pop checksum from mnemonic
checksumWord = wlist.pop() as string;
}
// Decode mnemonic
for (let i = 0; i < wlist.length; i += 3) {
let w1;
let w2;
let 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 InvalidWordsError();
}
const x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n);
if (x % n !== w1) {
throw new DecodingError();
}
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 VerificationError();
}
}
return out;
}
const mnWords = {} as Record<
string,
{
prefixLen: number;
words: any;
truncWords: Array<any>;
}
>;
mnWords.english = {
prefixLen: 3,
// eslint-disable-next-line global-require
words: require('../../../mnemonic_languages/english.json'),
truncWords: [],
};
export function get_languages(): Array<string> {
return Object.keys(mnWords);
}
// eslint-disable-next-line no-restricted-syntax
for (const i in mnWords) {
// eslint-disable-next-line no-prototype-builtins
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));
}
}
}