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/util/accountManager.ts

231 lines
8.5 KiB
TypeScript

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';
import {
removeAllContactPreKeys,
removeAllContactSignedPreKeys,
removeAllPreKeys,
removeAllSessions,
removeAllSignedPreKeys,
} 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';
import { ConversationTypeEnum } from '../models/conversation';
/**
* Might throw
*/
export async function sessionGenerateKeyPair(
seed: ArrayBuffer
): Promise<{ pubKey: ArrayBufferLike; privKey: ArrayBufferLike }> {
const sodium = await getSodium();
const ed25519KeyPair = sodium.crypto_sign_seed_keypair(new Uint8Array(seed));
const x25519PublicKey = sodium.crypto_sign_ed25519_pk_to_curve25519(ed25519KeyPair.publicKey);
// prepend version byte (coming from `processKeys(raw_keys)`)
const origPub = new Uint8Array(x25519PublicKey);
const prependedX25519PublicKey = new Uint8Array(33);
prependedX25519PublicKey.set(origPub, 1);
prependedX25519PublicKey[0] = 5;
const x25519SecretKey = sodium.crypto_sign_ed25519_sk_to_curve25519(ed25519KeyPair.privateKey);
// prepend with 05 the public key
const x25519KeyPair = {
pubKey: prependedX25519PublicKey.buffer,
privKey: x25519SecretKey.buffer,
ed25519KeyPair,
};
return x25519KeyPair;
}
const generateKeypair = async (mnemonic: string, mnemonicLanguage: string) => {
let seedHex = 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);
return sessionGenerateKeyPair(seed);
};
/**
* Sign in with a recovery phrase. We won't try to recover an existing profile name
* @param mnemonic the mnemonic the user duly saved in a safe place. We will restore his sessionID based on this.
* @param mnemonicLanguage 'english' only is supported
* @param profileName the displayName to use for this user
*/
export async function signInWithRecovery(
mnemonic: string,
mnemonicLanguage: string,
profileName: string
) {
return registerSingleDevice(mnemonic, mnemonicLanguage, profileName);
}
/**
* Sign in with a recovery phrase but trying to recover display name and avatar from the first encountered configuration message.
* @param mnemonic the mnemonic the user duly saved in a safe place. We will restore his sessionID based on this.
* @param mnemonicLanguage 'english' only is supported
*/
export async function signInByLinkingDevice(mnemonic: string, mnemonicLanguage: string) {
if (!mnemonic) {
throw new Error('Session always needs a mnemonic. Either generated or given by the user');
}
if (!mnemonicLanguage) {
throw new Error('We always needs a mnemonicLanguage');
}
const identityKeyPair = await generateKeypair(mnemonic, mnemonicLanguage);
UserUtils.setSignInByLinking(true);
await createAccount(identityKeyPair);
UserUtils.saveRecoveryPhrase(mnemonic);
await clearSessionsAndPreKeys();
const pubKeyString = toHex(identityKeyPair.pubKey);
// await for the first configuration message to come in.
await registrationDone(pubKeyString, '');
}
/**
* This is a signup. User has no recovery and does not try to link a device
* @param mnemonic The mnemonic generated on first app loading and to use for this brand new user
* @param mnemonicLanguage only 'english' is supported
* @param profileName the display name to register toi
*/
export async function registerSingleDevice(
generatedMnemonic: string,
mnemonicLanguage: string,
profileName: string
) {
if (!generatedMnemonic) {
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(generatedMnemonic, mnemonicLanguage);
await createAccount(identityKeyPair);
UserUtils.saveRecoveryPhrase(generatedMnemonic);
await clearSessionsAndPreKeys();
await UserUtils.setLastProfileUpdateTimestamp(Date.now());
const pubKeyString = toHex(identityKeyPair.pubKey);
await registrationDone(pubKeyString, profileName);
}
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 mn_encode(hex);
}
export async function clearSessionsAndPreKeys() {
window.log.info('clearing all sessions');
// During secondary device registration we need to keep our prekeys sent
// to other pubkeys
await Promise.all([
removeAllPreKeys(),
removeAllSignedPreKeys(),
removeAllContactPreKeys(),
removeAllContactSignedPreKeys(),
removeAllSessions(),
]);
}
export async function deleteAccount(reason?: string) {
const deleteEverything = async () => {
window.log.info('configuration message sent successfully. Deleting everything');
await window.Signal.Logs.deleteAll();
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
await window.Signal.Data.removeDB();
await window.Signal.Data.removeOtherData();
// 'unlink' => toast will be shown on app restart
window.localStorage.setItem('restart-reason', reason || '');
};
try {
window.log.info('DeleteAccount => Sending a last SyncConfiguration');
// be sure to wait for the message being effectively sent. Otherwise we won't be able to encrypt it for our devices !
await forceSyncConfigurationNowIfNeeded(true);
window.log.info('Last configuration message sent!');
await deleteEverything();
} catch (error) {
window.log.error(
'Something went wrong deleting all data:',
error && error.stack ? error.stack : error
);
try {
await deleteEverything();
} catch (e) {
window.log.error(e);
}
}
window.restart();
}
async function 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);
}
async function registrationDone(ourPubkey: string, displayName: string) {
window.log.info('registration done');
window.textsecure.storage.put('primaryDevicePubKey', ourPubkey);
// Ensure that we always have a conversation for ourself
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
ourPubkey,
ConversationTypeEnum.PRIVATE
);
await conversation.setLokiProfile({ displayName });
const user = {
ourNumber: getOurPubKeyStrFromCache(),
ourPrimary: window.textsecure.storage.get('primaryDevicePubKey'),
};
window.inboxStore?.dispatch(userActions.userChanged(user));
window.Whisper.Registration.markDone();
window.log.info('dispatching registration event');
trigger('registration_done');
}