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/MessageEncrypter.ts

135 lines
4.5 KiB
TypeScript

import { GroupPubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { concatUInt8Array, getSodiumRenderer } from '.';
import { Data } from '../../data/data';
import { SignalService } from '../../protobuf';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { MetaGroupWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { PubKey } from '../types';
import { UserUtils } from '../utils';
import { fromHexToArray } from '../utils/String';
import { SigningFailed } from '../utils/errors';
import { addMessagePadding } from './BufferPadding';
type EncryptResult = {
envelopeType: SignalService.Envelope.Type;
cipherText: Uint8Array;
};
async function encryptWithLibSession(destination: GroupPubkeyType, plainText: Uint8Array) {
try {
return (await MetaGroupWrapperActions.encryptMessages(destination, [plainText]))[0];
} catch (e) {
window.log.warn('encrypt message for group failed with', e.message);
throw new SigningFailed(e.message);
}
}
async function encryptForLegacyGroup(destination: PubKey, plainText: Uint8Array) {
const hexEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(destination.key);
if (!hexEncryptionKeyPair) {
window?.log?.warn("Couldn't get key pair for closed group during encryption");
throw new Error("Couldn't get key pair for closed group");
}
const destinationX25519Pk = PubKey.cast(hexEncryptionKeyPair.publicHex);
const cipherTextClosedGroup = await MessageEncrypter.encryptUsingSessionProtocol(
destinationX25519Pk,
plainText
);
return {
envelopeType: SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE,
cipherText: cipherTextClosedGroup,
};
}
/**
* Encrypt `plainTextBuffer` with given `encryptionType` for `destination`.
*
* @param destination The device `PubKey` to encrypt for.
* @param plainTextBuffer The unpadded plaintext buffer. It will be padded
* @param encryptionType The type of encryption.
* @returns The envelope type and the base64 encoded cipher text
*/
// eslint-disable-next-line consistent-return
async function encrypt(
destination: PubKey,
plainTextBuffer: Uint8Array,
encryptionType: SignalService.Envelope.Type
): Promise<EncryptResult> {
const { CLOSED_GROUP_MESSAGE, SESSION_MESSAGE } = SignalService.Envelope.Type;
const plainTextPadded = addMessagePadding(plainTextBuffer);
switch (encryptionType) {
case SESSION_MESSAGE: {
const cipherText = await MessageEncrypter.encryptUsingSessionProtocol(
PubKey.cast(destination.key),
plainTextPadded
);
return { envelopeType: SESSION_MESSAGE, cipherText };
}
case CLOSED_GROUP_MESSAGE: {
const groupPk = destination.key;
if (PubKey.is03Pubkey(groupPk)) {
return {
envelopeType: CLOSED_GROUP_MESSAGE,
cipherText: await encryptWithLibSession(groupPk, plainTextBuffer),
};
}
return encryptForLegacyGroup(destination, plainTextPadded); // not padding it again, it is already done by libsession
}
default:
assertUnreachable(encryptionType, 'MessageEncrypter encrypt unreachable case');
}
}
async function encryptUsingSessionProtocol(
destinationX25519Pk: PubKey,
plaintext: Uint8Array
): Promise<Uint8Array> {
const userED25519KeyPairHex = await UserUtils.getUserED25519KeyPair();
if (
!userED25519KeyPairHex ||
!userED25519KeyPairHex.pubKey?.length ||
!userED25519KeyPairHex.privKey?.length
) {
throw new Error("Couldn't find user ED25519 key pair.");
}
const sodium = await getSodiumRenderer();
const recipientX25519PublicKey = fromHexToArray(
PubKey.removePrefixIfNeeded(destinationX25519Pk.key)
);
const userED25519PubKeyBytes = fromHexToArray(userED25519KeyPairHex.pubKey);
const userED25519SecretKeyBytes = fromHexToArray(userED25519KeyPairHex.privKey);
// merge all arrays into one
const verificationData = concatUInt8Array(
plaintext,
userED25519PubKeyBytes,
recipientX25519PublicKey
);
const signature = sodium.crypto_sign_detached(verificationData, userED25519SecretKeyBytes);
if (isEmpty(signature)) {
throw new Error("Couldn't sign message");
}
const plaintextWithMetadata = concatUInt8Array(plaintext, userED25519PubKeyBytes, signature);
const ciphertext = sodium.crypto_box_seal(plaintextWithMetadata, recipientX25519PublicKey);
if (isEmpty(ciphertext)) {
throw new Error("Couldn't encrypt message.");
}
return ciphertext;
}
export const MessageEncrypter = {
encryptUsingSessionProtocol,
encrypt,
};