Move accountManager to typescript

This commit is contained in:
Audric Ackermann 2021-03-01 10:16:15 +11:00
parent fe684088de
commit 5d6d81b9ef
13 changed files with 280 additions and 261 deletions

View file

@ -83,7 +83,6 @@ module.exports = grunt => {
'libtextsecure/helpers.js',
'libtextsecure/stringview.js',
'libtextsecure/event_target.js',
'libtextsecure/account_manager.js',
'libtextsecure/http-resources.js',
'libtextsecure/message_receiver.js',
'libtextsecure/task_with_timeout.js',

View file

@ -96,27 +96,6 @@
Whisper.events = _.clone(Backbone.Events);
Whisper.events.isListenedTo = eventName =>
Whisper.events._events ? !!Whisper.events._events[eventName] : false;
let accountManager;
window.getAccountManager = () => {
if (!accountManager) {
const USERNAME = storage.get('number_id');
const PASSWORD = storage.get('password');
accountManager = new textsecure.AccountManager(USERNAME, PASSWORD);
accountManager.addEventListener('registration', () => {
const user = {
ourNumber: libsession.Utils.UserUtils.getOurPubKeyStrFromCache(),
ourPrimary: window.textsecure.storage.get('primaryDevicePubKey'),
};
Whisper.events.trigger('userChanged', user);
Whisper.Registration.markDone();
window.log.info('dispatching registration event');
Whisper.events.trigger('registration_done');
});
}
return accountManager;
};
const cancelInitializationMessage = Views.Initialization.setMessage();
window.log.info('Storage fetch');

View file

@ -1,162 +0,0 @@
/* global
window,
textsecure,
libsignal,
mnemonic,
btoa,
getString,
Event,
dcodeIO,
StringView,
Event,
*/
/* eslint-disable more/no-then */
/* eslint-disable no-unused-vars */
/* eslint-disable no-await-in-loop */
// eslint-disable-next-line func-names
(function() {
window.textsecure = window.textsecure || {};
const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000;
function AccountManager(username, password) {
this.pending = Promise.resolve();
}
function getNumber(numberId) {
if (!numberId || !numberId.length) {
return numberId;
}
const parts = numberId.split('.');
if (!parts.length) {
return numberId;
}
return parts[0];
}
AccountManager.prototype = new textsecure.EventTarget();
AccountManager.prototype.extend({
constructor: AccountManager,
registerSingleDevice(mnemonic, mnemonicLanguage, profileName) {
const createAccount = this.createAccount.bind(this);
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
const registrationDone = this.registrationDone.bind(this);
let generateKeypair;
if (mnemonic) {
generateKeypair = () => {
let seedHex = window.mnemonic.mn_decode(mnemonic, mnemonicLanguage);
// handle shorter than 32 bytes seeds
const privKeyHexLength = 32 * 2;
if (seedHex.length !== privKeyHexLength) {
seedHex = seedHex.concat('0'.repeat(32));
seedHex = seedHex.substring(0, privKeyHexLength);
}
const seed = dcodeIO.ByteBuffer.wrap(seedHex, 'hex').toArrayBuffer();
return window.sessionGenerateKeyPair(seed);
};
} else {
generateKeypair = libsignal.KeyHelper.generateIdentityKeyPair;
}
return this.queueTask(() =>
generateKeypair().then(async identityKeyPair =>
createAccount(identityKeyPair)
.then(() => this.saveRecoveryPhrase(mnemonic))
.then(clearSessionsAndPreKeys)
.then(() => {
const pubKeyString = StringView.arrayBufferToHex(
identityKeyPair.pubKey
);
registrationDone(pubKeyString, profileName);
})
)
);
},
queueTask(task) {
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
this.pending = this.pending.then(taskWithTimeout, taskWithTimeout);
return this.pending;
},
async createAccount(identityKeyPair, userAgent, readReceipts) {
let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2);
await Promise.all([
textsecure.storage.remove('identityKey'),
textsecure.storage.remove('signaling_key'),
textsecure.storage.remove('password'),
textsecure.storage.remove('registrationId'),
textsecure.storage.remove('number_id'),
textsecure.storage.remove('device_name'),
textsecure.storage.remove('userAgent'),
textsecure.storage.remove('read-receipt-setting'),
textsecure.storage.remove('typing-indicators-setting'),
textsecure.storage.remove('regionCode'),
]);
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
const pubKeyString = StringView.arrayBufferToHex(identityKeyPair.pubKey);
await textsecure.storage.put('identityKey', identityKeyPair);
await textsecure.storage.put('password', password);
if (userAgent) {
await textsecure.storage.put('userAgent', userAgent);
}
await textsecure.storage.put(
'read-receipt-setting',
Boolean(readReceipts)
);
// Enable typing indicators by default
await textsecure.storage.put('typing-indicators-setting', Boolean(true));
await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
},
async clearSessionsAndPreKeys() {
window.log.info('clearing all sessions');
// During secondary device registration we need to keep our prekeys sent
// to other pubkeys
await Promise.all([
window.Signal.Data.removeAllPreKeys(),
window.Signal.Data.removeAllSignedPreKeys(),
window.Signal.Data.removeAllContactPreKeys(),
window.Signal.Data.removeAllContactSignedPreKeys(),
window.Signal.Data.removeAllSessions(),
]);
},
async generateMnemonic(language = 'english') {
// 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 = window.Signal.Crypto.getRandomBytes(seedSize);
const hex = StringView.arrayBufferToHex(seed);
return mnemonic.mn_encode(hex, language);
},
getCurrentRecoveryPhrase() {
return textsecure.storage.get('mnemonic');
},
saveRecoveryPhrase(mnemonic) {
return textsecure.storage.put('mnemonic', mnemonic);
},
async registrationDone(number, displayName) {
window.log.info('registration done');
textsecure.storage.put('primaryDevicePubKey', number);
// Ensure that we always have a conversation for ourself
const conversation = await window
.getConversationController()
.getOrCreateAndWait(number, 'private');
await conversation.setLokiProfile({ displayName });
this.dispatchEvent(new Event('registration'));
},
});
textsecure.AccountManager = AccountManager;
})();

View file

@ -5,6 +5,7 @@
<title>libtextsecure test runner</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="mocha">
@ -25,13 +26,11 @@
<script type="text/javascript" src="../event_target.js" data-cover></script>
<script type="text/javascript" src="../helpers.js" data-cover></script>
<script type="text/javascript" src="../stringview.js" data-cover></script>
<script type="text/javascript" src="../account_manager.js" data-cover></script>
<script type="text/javascript" src="../task_with_timeout.js" data-cover></script>
<script type="text/javascript" src="errors_test.js"></script>
<script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="task_with_timeout_test.js"></script>
<script type="text/javascript" src="account_manager_test.js"></script>
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
<!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->
@ -42,4 +41,5 @@
mocha.run();
</script>
</body>
</html>
</html>

View file

@ -86,7 +86,7 @@ window.isBeforeVersion = (toCheck, baseVersion) => {
};
// eslint-disable-next-line func-names
window.CONSTANTS = new (function() {
window.CONSTANTS = new (function () {
this.MAX_GROUP_NAME_LENGTH = 64;
this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer');
this.MAX_LINKED_DEVICES = 1;
@ -184,6 +184,7 @@ window.libsession = require('./ts/session');
window.getConversationController =
window.libsession.Conversations.ConversationController.getInstance;
// We never do these in our code, so we'll prevent it everywhere
window.open = () => null;
// eslint-disable-next-line no-eval, no-multi-assign
@ -377,7 +378,7 @@ window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args);
// Linux seems to periodically let the event loop stop, so this is a global workaround
setInterval(() => {
window.nodeSetImmediate(() => {});
window.nodeSetImmediate(() => { });
}, 1000);
const { autoOrientImage } = require('./js/modules/auto_orient_image');
@ -458,9 +459,9 @@ if (process.env.USE_STUBBED_NETWORK) {
}
// eslint-disable-next-line no-extend-native,func-names
Promise.prototype.ignore = function() {
Promise.prototype.ignore = function () {
// eslint-disable-next-line more/no-then
this.then(() => {});
this.then(() => { });
};
if (

View file

@ -14,13 +14,13 @@ import { getTheme } from '../../state/selectors/theme';
import { getOurNumber } from '../../state/selectors/user';
import { UserUtils } from '../../session/utils';
import {
forceSyncConfigurationNowIfNeeded,
syncConfigurationIfNeeded,
} from '../../session/utils/syncUtils';
import { DAYS } from '../../session/utils/Number';
import { removeItemById } from '../../data/data';
import { OnionPaths } from '../../session/onions';
import { getMessageQueue } from '../../session/sending';
import { AccountManager } from '../../util';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType {
@ -77,7 +77,7 @@ class ActionsPanelPrivate extends React.Component<Props> {
void this.showResetSessionIDDialogIfNeeded();
// remove existing prekeys, sign prekeys and sessions
void window.getAccountManager().clearSessionsAndPreKeys();
void AccountManager.clearSessionsAndPreKeys();
// trigger a sync message if needed for our other devices
void syncConfigurationIfNeeded();

View file

@ -2,7 +2,7 @@ import React from 'react';
import { SessionModal } from './SessionModal';
import { SessionButton } from './SessionButton';
import { ToastUtils } from '../../session/utils';
import { ToastUtils, UserUtils } from '../../session/utils';
import { DefaultTheme, withTheme } from 'styled-components';
import { PasswordUtil } from '../../util';
import { getPasswordHash } from '../../data/data';
@ -70,8 +70,8 @@ class SessionSeedModalInner extends React.Component<Props, State> {
{hasPassword && !passwordValid ? (
<>{this.renderPasswordView()}</>
) : (
<>{this.renderSeedView()}</>
)}
<>{this.renderSeedView()}</>
)}
</SessionModal>
)}
</>
@ -192,8 +192,7 @@ class SessionSeedModalInner extends React.Component<Props, State> {
return false;
}
const manager = await window.getAccountManager();
const recoveryPhrase = manager.getCurrentRecoveryPhrase();
const recoveryPhrase = UserUtils.getCurrentRecoveryPhrase();
this.setState({
recoveryPhrase,

View file

@ -8,6 +8,7 @@ import { SignInTab } from './SignInTab';
import { TabLabel, TabType } from './TabLabel';
import { PasswordUtil } from '../../../util';
import { trigger } from '../../../shims/events';
import { AccountManager } from '../../../util/accountManager';
export const MAX_USERNAME_LENGTH = 20;
// tslint:disable: use-simple-attributes
@ -70,7 +71,7 @@ export async function signUp(signUpDetails: {
verifyPassword,
generatedRecoveryPhrase,
} = signUpDetails;
window.log.info('starting Signing up');
window.log.info('SIGNING UP');
const trimName = displayName.trim();
if (!trimName) {
@ -103,9 +104,77 @@ export async function signUp(signUpDetails: {
await resetRegistration();
await window.setPassword(password);
UserUtils.setRestoringFromSeed(false);
await window
.getAccountManager()
.registerSingleDevice(generatedRecoveryPhrase, 'english', trimName);
await AccountManager.registerSingleDevice(
generatedRecoveryPhrase,
'english',
trimName
);
await UserUtils.setLastProfileUpdateTimestamp(Date.now());
trigger('openInbox');
} catch (e) {
ToastUtils.pushToastError(
'registrationError',
`Error: ${e.message || 'Something went wrong'}`
);
window.log.warn('exception during registration:', e);
}
}
/**
* Sign in/restore from seed.
* Ask for a display name, as we will drop incoming ConfigurationMessages if any are saved on the swarm.
* We will handle a ConfigurationMessage
*/
export async function restoreFromSeed(signInDetails: {
displayName: string;
userRecoveryPhrase: string;
password: string;
verifyPassword: string;
}) {
const {
displayName,
password,
verifyPassword,
userRecoveryPhrase,
} = signInDetails;
window.log.info('RESTORING FROM SEED');
const trimName = displayName.trim();
if (!trimName) {
window.log.warn('invalid trimmed name for registration');
ToastUtils.pushToastError(
'invalidDisplayName',
window.i18n('displayNameEmpty')
);
return;
}
const passwordErrors = validatePassword(password, verifyPassword);
if (passwordErrors.passwordErrorString) {
window.log.warn('invalid password for registration');
ToastUtils.pushToastError(
'invalidPassword',
window.i18n('invalidPassword')
);
return;
}
if (!!password && !passwordErrors.passwordFieldsMatch) {
window.log.warn('passwords does not match for registration');
ToastUtils.pushToastError(
'invalidPassword',
window.i18n('passwordsDoNotMatch')
);
return;
}
try {
await resetRegistration();
await window.setPassword(password);
UserUtils.setRestoringFromSeed(false);
await AccountManager.registerSingleDevice(
userRecoveryPhrase,
'english',
trimName
);
await UserUtils.setLastProfileUpdateTimestamp(Date.now());
trigger('openInbox');
} catch (e) {
@ -157,9 +226,7 @@ export class RegistrationTabs extends React.Component<any, State> {
private async generateMnemonicAndKeyPair() {
if (this.state.generatedRecoveryPhrase === '') {
const language = 'english';
const mnemonic = await window
.getAccountManager()
.generateMnemonic(language);
const mnemonic = await AccountManager.generateMnemonic(language);
let seedHex = window.mnemonic.mn_decode(mnemonic, language);
// handle shorter than 32 bytes seeds
@ -206,58 +273,48 @@ export class RegistrationTabs extends React.Component<any, State> {
return <SignInTab />;
}
private async register() {
// const {
// password,
// recoveryPhrase,
// generatedRecoveryPhrase,
// signInMode,
// displayName,
// passwordErrorString,
// passwordFieldsMatch,
// } = this.state;
// if (signInMode === SignInMode.UsingRecoveryPhrase && !recoveryPhrase) {
// window.log.warn('empty mnemonic seed passed in seed restoration mode');
// return;
// } else if (!generatedRecoveryPhrase) {
// window.log.warn('empty generated seed');
// return;
// }
// const seedToUse =
// signInMode === SignInMode.UsingRecoveryPhrase
// ? recoveryPhrase
// : generatedRecoveryPhrase;
// try {
// await this.resetRegistration();
// await window.setPassword(password);
// const isRestoringFromSeed = signInMode === SignInMode.UsingRecoveryPhrase;
// UserUtils.setRestoringFromSeed(isRestoringFromSeed);
// await window
// .getAccountManager()
// .registerSingleDevice(seedToUse, 'english', trimName);
// // if we are just creating a new account, no need to wait for a configuration message
// if (!isRestoringFromSeed) {
// trigger('openInbox');
// } else {
// // We have to pull for all messages of the user of this menmonic
// // We are looking for the most recent ConfigurationMessage he sent to himself.
// // When we find it, we can just get the displayName, avatar and groups saved in it.
// // If we do not find one, we will need to ask for a display name.
// window.log.warn('isRestoringFromSeed');
// }
// } catch (e) {
// ToastUtils.pushToastError(
// 'registrationError',
// `Error: ${e.message || 'Something went wrong'}`
// );
// let exmsg = '';
// if (e.message) {
// exmsg += e.message;
// }
// if (e.stack) {
// exmsg += ` | stack: + ${e.stack}`;
// }
// window.log.warn('exception during registration:', exmsg);
// }
}
// private async register() {
// const {
// password,
// recoveryPhrase,
// generatedRecoveryPhrase,
// signInMode,
// displayName,
// passwordErrorString,
// passwordFieldsMatch,
// } = this.state;
// if (signInMode === SignInMode.UsingRecoveryPhrase && !recoveryPhrase) {
// window.log.warn('empty mnemonic seed passed in seed restoration mode');
// return;
// } else if (!generatedRecoveryPhrase) {
// window.log.warn('empty generated seed');
// return;
// }
// const seedToUse =
// signInMode === SignInMode.UsingRecoveryPhrase
// ? recoveryPhrase
// : generatedRecoveryPhrase;
// try {
// await this.resetRegistration();
// await window.setPassword(password);
// const isRestoringFromSeed = signInMode === SignInMode.UsingRecoveryPhrase;
// UserUtils.setRestoringFromSeed(isRestoringFromSeed);
// await AccountManager.registerSingleDevice(seedToUse, 'english', trimName);
// // if we are just creating a new account, no need to wait for a configuration message
// if (!isRestoringFromSeed) {
// trigger('openInbox');
// } else {
// // We have to pull for all messages of the user of this menmonic
// // We are looking for the most recent ConfigurationMessage he sent to himself.
// // When we find it, we can just get the displayName, avatar and groups saved in it.
// // If we do not find one, we will need to ask for a display name.
// window.log.warn('isRestoringFromSeed');
// }
// } catch (e) {
// ToastUtils.pushToastError(
// 'registrationError',
// `Error: ${e.message || 'Something went wrong'}`
// );
// }
// }
}

View file

@ -556,7 +556,10 @@ export async function getConversationById(
id: string
): Promise<ConversationModel | undefined> {
const data = await channels.getConversationById(id);
return new ConversationModel(data);
if (data) {
return new ConversationModel(data);
}
return undefined;
}
export async function updateConversation(

View file

@ -115,3 +115,11 @@ export function setLastProfileUpdateTimestamp(lastUpdateTimestamp: number) {
lastUpdateTimestamp
);
}
export function getCurrentRecoveryPhrase() {
return window.textsecure.storage.get('mnemonic');
}
export function saveRecoveryPhrase(mnemonic: string) {
return window.textsecure.storage.put('mnemonic', mnemonic);
}

135
ts/util/accountManager.ts Normal file
View file

@ -0,0 +1,135 @@
import { ConversationController } from '../session/conversations';
import { getSodium } from '../session/crypto';
import { UserUtils } from '../session/utils';
import {
fromArrayBufferToBase64,
fromHex,
toHex,
} from '../session/utils/String';
import { getOurPubKeyStrFromCache } from '../session/utils/User';
import { trigger } from '../shims/events';
const generateKeypair = async (mnemonic: string, mnemonicLanguage: string) => {
let seedHex = window.mnemonic.mn_decode(mnemonic, mnemonicLanguage);
// handle shorter than 32 bytes seeds
const privKeyHexLength = 32 * 2;
if (seedHex.length !== privKeyHexLength) {
seedHex = seedHex.concat('0'.repeat(32));
seedHex = seedHex.substring(0, privKeyHexLength);
}
const seed = fromHex(seedHex);
console.warn('generateKeypair seedHex', seedHex);
console.warn('generateKeypair seed', seed);
return window.sessionGenerateKeyPair(seed);
};
// TODO not sure why AccountManager was a singleton before. Can we get rid of it as a singleton?
// tslint:disable-next-line: no-unnecessary-class
export class AccountManager {
public static async registerSingleDevice(
mnemonic: string,
mnemonicLanguage: string,
profileName: string
) {
const createAccount = this.createAccount.bind(this);
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
const registrationDone = this.registrationDone.bind(this);
if (!mnemonic) {
throw new Error(
'Session always needs a mnemonic. Either generated or given by the user'
);
}
if (!profileName) {
throw new Error('We always needs a profileName');
}
if (!mnemonicLanguage) {
throw new Error('We always needs a mnemonicLanguage');
}
const identityKeyPair = await generateKeypair(mnemonic, mnemonicLanguage);
await createAccount(identityKeyPair);
UserUtils.saveRecoveryPhrase(mnemonic);
await clearSessionsAndPreKeys();
const pubKeyString = toHex(identityKeyPair.pubKey);
await registrationDone(pubKeyString, profileName);
}
public static async generateMnemonic(language = 'english') {
// 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 = window.Signal.Crypto.getRandomBytes(seedSize);
const hex = toHex(seed);
return window.mnemonic.mn_encode(hex, language);
}
public static async clearSessionsAndPreKeys() {
window.log.info('clearing all sessions');
// During secondary device registration we need to keep our prekeys sent
// to other pubkeys
await Promise.all([
window.Signal.Data.removeAllPreKeys(),
window.Signal.Data.removeAllSignedPreKeys(),
window.Signal.Data.removeAllContactPreKeys(),
window.Signal.Data.removeAllContactSignedPreKeys(),
window.Signal.Data.removeAllSessions(),
]);
}
private static async createAccount(identityKeyPair: any) {
const sodium = await getSodium();
let password = fromArrayBufferToBase64(sodium.randombytes_buf(16));
password = password.substring(0, password.length - 2);
await Promise.all([
window.textsecure.storage.remove('identityKey'),
window.textsecure.storage.remove('signaling_key'),
window.textsecure.storage.remove('password'),
window.textsecure.storage.remove('registrationId'),
window.textsecure.storage.remove('number_id'),
window.textsecure.storage.remove('device_name'),
window.textsecure.storage.remove('userAgent'),
window.textsecure.storage.remove('read-receipt-setting'),
window.textsecure.storage.remove('typing-indicators-setting'),
window.textsecure.storage.remove('regionCode'),
]);
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
const pubKeyString = toHex(identityKeyPair.pubKey);
await window.textsecure.storage.put('identityKey', identityKeyPair);
await window.textsecure.storage.put('password', password);
await window.textsecure.storage.put('read-receipt-setting', false);
// Enable typing indicators by default
await window.textsecure.storage.put(
'typing-indicators-setting',
Boolean(true)
);
await window.textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
}
private static async registrationDone(number: string, displayName: string) {
window.log.info('registration done');
window.textsecure.storage.put('primaryDevicePubKey', number);
// Ensure that we always have a conversation for ourself
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
number,
'private'
);
await conversation.setLokiProfile({ displayName });
const user = {
ourNumber: getOurPubKeyStrFromCache(),
ourPrimary: window.textsecure.storage.get('primaryDevicePubKey'),
};
trigger('userChanged', user);
window.Whisper.Registration.markDone();
window.log.info('dispatching registration event');
trigger('registration_done');
}
}

View file

@ -10,6 +10,7 @@ import * as AttachmentUtil from './attachmentsUtil';
import * as LinkPreviewUtil from './linkPreviewFetch';
export * from './blockedNumberController';
export * from './accountManager';
export {
arrayBufferToObjectURL,

1
ts/window.d.ts vendored
View file

@ -49,7 +49,6 @@ declare global {
deleteAccount: any;
displayNameRegex: any;
friends: any;
getAccountManager: any;
getConversations: any;
getFriendsFromContacts: any;
getSettingValue: any;