/** * Rawr-X3DH -- eXtended 3-way Diffie-Hellman * * Specification by Open Whisper Systems * Powered by Libsodium * * Implemented by Soatok Dreamseeker * * ................................:................. * .............................-+yd-................ * ............/+:-.....+/oys++://:m:................ * --........../y///oyssyyyyhddh+-:y/................ * --------.....o--+syyso/syyyhho:--+..........:-.... * ----------.....:/ssss+ooyoosyo//yo.--------oy:.... * --------------:+//+//++:`-/o-syyy/-------+yo------ * --------------:oy++:s. ++:+: `ys/------/ss:------- * ---------------:+++syh--/++++oss//---:oy+--------- * ----------------:syyhhysosssyyyyhso/+yo:---------- * ----------------::shddyyhyyyyshdhyyyy/------------ * ----------------:shhhyyyssssssoyhhhyhho/::-------- * ::::::---------:+shhhddd+o+++++yddhhhhhyyyso+::::- * ::::::::::::o+oyssyhyhdh+///:+hhoshhhhhhhhyo+::::: * ::::::::::::+syyssss/:yyoo+sydo/o+s+/+osyysoo/:::: * ::::::::::::/+ssyyyyy/:oyyhhhs/ss/y/:::::::::::::: * :::::::::::::::/+syhhhsyhhhhyyss+oo/:::::::::::::: * :::::::::/o+/:::::/+syhddddysyso+so/:::::////::::: * ::::::+yhhyhhs::::::/yhdddddssso/ss:::::::///::::: * :::::::::hhhh+/+/:/shoshyysss/ `+s:::::://::::/:: * ::::::::+hhhho:+sshhsyhhhs+:. `-//:::::::::::: * ::::::::ohhyoo+oyhyyhhhyyssoo/:-` .:/:::::::::: * :::::::::syso+syhhhhhhhhhhhhhhyyyo:` ./::::::::: * :::::::::--://+o++osyo+yhhhhhhhhhhyys/` ::::::::: * ::::::--.:/+/::::-::::yhhhhhhhhhhhyyy+. ::::::::: * :------://:::::----:::/yhyyyyyyyyyys+` :+::::::: * ------::-----------:shyhhyyyyss+/:-...-::+//:::::: * ------------------/yhhhhhhyyyssso+:::::::::::::::: * -----------------+yyyhhhhhhhyyssso+/---------::::: * ---------------/syyys/yhhhhhhyyyysss+------------- * .............:syyyyo---oyhhhhhhhhyyyhs------------ * ...........-oyyyyyo....-+syyhyyhhhhddy------------ * ...........syyyyys-......-::::+:////:.------------ * ...........yyyyys:............-...............---- * ...........+sss:.................................. * .....````````.``.................................. * */ import type { Ed25519SecretKey, X25519SecretKey } from 'sodium-plus' import { CryptographyKey, Ed25519PublicKey, SodiumPlus, X25519PublicKey } from 'sodium-plus' import type { KeyDerivationFunction, SymmetricEncryptionInterface } from './src/symmetric' import { blakeKdf, SymmetricCrypto } from './src/symmetric' import type { SessionKeyManagerInterface, IdentityKeyManagerInterface } from './src/persistence' import { DefaultSessionKeyManager, DefaultIdentityKeyManager } from './src/persistence' import { concat, generateKeyPair, generateBundle, signBundle, verifyBundle, wipe } from './src/util' /** * Initial server info. * * Contains the information necessary to complete * the X3DH handshake from a sender's side. */ export type InitServerInfo = { IdentityKey:string, SignedPreKey:{ Signature:string, PreKey:string }, OneTimeKey?:string }; /** * Initial information about a sender */ export type InitSenderInfo = { Sender:string, IdentityKey:string, EphemeralKey:string, OneTimeKey?:string, CipherText:string }; /** * Send a network request to the server to obtain the public keys needed * to complete the sender's handshake. */ export type InitClientFunction = (id:string)=>Promise; /** * Signed key bundle. */ export type SignedBundle = { signature:string, bundle:string[] }; /** * Initialization information for receiving a handshake message. */ type RecipientInitWithSK = { IK:Ed25519PublicKey, EK:X25519PublicKey, SK:CryptographyKey, OTK?:string }; /** * Pluggable X3DH implementation, powered by libsodium. */ export class X3DH { encryptor:SymmetricEncryptionInterface kdf:KeyDerivationFunction identityKeyManager:IdentityKeyManagerInterface sessionKeyManager:SessionKeyManagerInterface sodium?:SodiumPlus constructor ( identityKeyManager?:IdentityKeyManagerInterface, sessionKeyManager?:SessionKeyManagerInterface, encryptor?:SymmetricEncryptionInterface, kdf?:KeyDerivationFunction ) { if (!sessionKeyManager) { sessionKeyManager = new DefaultSessionKeyManager() } if (!identityKeyManager) { identityKeyManager = new DefaultIdentityKeyManager() } if (!encryptor) { encryptor = new SymmetricCrypto() } if (!kdf) { kdf = blakeKdf } this.encryptor = encryptor this.kdf = kdf this.sessionKeyManager = sessionKeyManager this.identityKeyManager = identityKeyManager } /** * @returns {SodiumPlus} */ async getSodium ():Promise { if (!this.sodium) { this.sodium = await SodiumPlus.auto() } return this.sodium } /** * Generates and signs a bundle of one-time keys. * * Useful for pushing more OTKs to the server. * * @param {Ed25519SecretKey} signingKey * @param {number} numKeys */ async generateOneTimeKeys ( signingKey:Ed25519SecretKey, numKeys:number = 100 ):Promise { const sodium = await this.getSodium() const bundle = await generateBundle(numKeys) const publicKeys = bundle.map(x => x.publicKey) const signature = await signBundle(signingKey, publicKeys) await this.identityKeyManager.persistOneTimeKeys(bundle) // Hex-encode all the public keys const encodedBundle : string[] = [] for (const pk of publicKeys) { encodedBundle.push(await sodium.sodium_bin2hex(pk.getBuffer())) } return { signature: await sodium.sodium_bin2hex(signature), bundle: encodedBundle } } /** * Get the shared key when sending an initial message. * * @param {InitServerInfo} res * @param {Ed25519SecretKey} senderKey */ async initSenderGetSK ( res:InitServerInfo, senderKey:Ed25519SecretKey ):Promise { const sodium = await this.getSodium() const identityKey = new Ed25519PublicKey( await sodium.sodium_hex2bin(res.IdentityKey) ) const signedPreKey = new X25519PublicKey( await sodium.sodium_hex2bin(res.SignedPreKey.PreKey) ) const signature = await sodium.sodium_hex2bin(res.SignedPreKey.Signature) // Check signature const valid = await verifyBundle(identityKey, [signedPreKey], signature) if (!valid) { throw new Error('Invalid signature') } const ephemeral = await generateKeyPair() const ephSecret = ephemeral.secretKey const ephPublic = ephemeral.publicKey // Turn the Ed25519 keys into X25519 keys for X3DH: const senderX = await sodium.crypto_sign_ed25519_sk_to_curve25519(senderKey) const recipientX = await sodium.crypto_sign_ed25519_pk_to_curve25519(identityKey) // See the X3DH specification to really understand this part: const DH1 = await sodium.crypto_scalarmult(senderX, signedPreKey) const DH2 = await sodium.crypto_scalarmult(ephSecret, recipientX) const DH3 = await sodium.crypto_scalarmult(ephSecret, signedPreKey) let SK if (res.OneTimeKey) { const DH4 = await sodium.crypto_scalarmult( ephSecret, new X25519PublicKey(await sodium.sodium_hex2bin(res.OneTimeKey)) ) SK = new CryptographyKey( Buffer.from(await this.kdf( concat( DH1.getBuffer(), DH2.getBuffer(), DH3.getBuffer(), DH4.getBuffer() ) )) ) await wipe(DH4) } else { SK = new CryptographyKey( Buffer.from(await this.kdf( concat( DH1.getBuffer(), DH2.getBuffer(), DH3.getBuffer() ) )) ) } // Wipe DH keys since we have SK await wipe(DH1) await wipe(DH2) await wipe(DH3) await wipe(ephSecret) await wipe(senderX) return { IK: identityKey, EK: ephPublic, SK, OTK: res.OneTimeKey } } /** * Initialize for sending. * * @param {string} recipientIdentity * @param {InitClientFunction} getServerResponse * @param {string|Buffer} message */ async initSend ( recipientIdentity:string, getServerResponse:InitClientFunction, message:string|Buffer ):Promise { const sodium = await this.getSodium() // Get the identity key for the sender: const senderIdentity = await this.identityKeyManager.getMyIdentityString() const identity = await this.identityKeyManager.getIdentityKeypair() const senderSecretKey = identity.identitySecret const senderPublicKey = identity.identityPublic // Stub out a call to get the server response: const response = await getServerResponse(recipientIdentity) // Get the shared symmetric key (and other handshake data): const { IK, EK, SK, OTK } = await this.initSenderGetSK(response, senderSecretKey) // Get the assocData for AEAD: const assocData = await sodium.sodium_bin2hex( Buffer.concat([senderPublicKey.getBuffer(), IK.getBuffer()]) ) // Set the session key (as a sender): await this.sessionKeyManager.setSessionKey(recipientIdentity, SK, false) await this.sessionKeyManager.setAssocData(recipientIdentity, assocData) return { Sender: senderIdentity, IdentityKey: await sodium.sodium_bin2hex(senderPublicKey.getBuffer()), EphemeralKey: await sodium.sodium_bin2hex(EK.getBuffer()), OneTimeKey: OTK, CipherText: await this.encryptor.encrypt( message, await this.sessionKeyManager.getEncryptionKey(recipientIdentity), assocData ) } } /** * Get the shared key when receiving an initial message. * * @param {InitSenderInfo} req * @param {Ed25519SecretKey} identitySecret * @param preKeySecret */ async initRecvGetSk ( req:InitSenderInfo, identitySecret:Ed25519SecretKey, preKeySecret:X25519SecretKey ) { const sodium = await this.getSodium() // Decode strings const senderIdentityKey = new Ed25519PublicKey( await sodium.sodium_hex2bin(req.IdentityKey), ) const ephemeral = new X25519PublicKey( await sodium.sodium_hex2bin(req.EphemeralKey), ) // Ed25519 -> X25519 const senderX = await sodium.crypto_sign_ed25519_pk_to_curve25519(senderIdentityKey) const recipientX = await sodium.crypto_sign_ed25519_sk_to_curve25519(identitySecret) // See the X3DH specification to really understand this part: const DH1 = await sodium.crypto_scalarmult(preKeySecret, senderX) const DH2 = await sodium.crypto_scalarmult(recipientX, ephemeral) const DH3 = await sodium.crypto_scalarmult(preKeySecret, ephemeral) let SK if (req.OneTimeKey) { const DH4 = await sodium.crypto_scalarmult( await this.identityKeyManager.fetchAndWipeOneTimeSecretKey(req.OneTimeKey), ephemeral ) SK = new CryptographyKey( Buffer.from(await this.kdf( concat( DH1.getBuffer(), DH2.getBuffer(), DH3.getBuffer(), DH4.getBuffer() ) )) ) await wipe(DH4) } else { SK = new CryptographyKey( Buffer.from(await this.kdf( concat( DH1.getBuffer(), DH2.getBuffer(), DH3.getBuffer() ) )) ) } // Wipe DH keys since we have SK await wipe(DH1) await wipe(DH2) await wipe(DH3) await wipe(recipientX) return { Sender: req.Sender, SK, IK: senderIdentityKey } } /** * Initialize keys for receiving an initial message. * Returns the initial plaintext message on success. * Throws on failure. * * @param {InitSenderInfo} req * @returns {(string|Buffer)[]} */ async initRecv (req:InitSenderInfo):Promise<(string|Buffer)[]> { const sodium = await this.getSodium() const { identitySecret, identityPublic } = await this.identityKeyManager.getIdentityKeypair() const { preKeySecret } = await this.identityKeyManager.getPreKeypair() const { Sender, SK, IK } = await this.initRecvGetSk( req, identitySecret, preKeySecret ) const assocData = await sodium.sodium_bin2hex( Buffer.from(concat(IK.getBuffer(), identityPublic.getBuffer())) ) try { await this.sessionKeyManager.setSessionKey(Sender, SK, true) await this.sessionKeyManager.setAssocData(Sender, assocData) return [ Sender, await this.encryptor.decrypt( req.CipherText, await this.sessionKeyManager.getEncryptionKey(Sender, true), assocData ) ] } catch (e) { // Decryption failure! Destroy the session. await this.sessionKeyManager.destroySessionKey(Sender) throw e } } /** * Encrypt the next message to send to the recipient. * * @param {string} recipient * @param {string|Buffer} message * @returns {string} */ async encryptNext (recipient:string, message:string|Buffer):Promise { return this.encryptor.encrypt( message, await this.sessionKeyManager.getEncryptionKey(recipient, false), await this.sessionKeyManager.getAssocData(recipient) ) } /** * Decrypt the next message received by the sender. * * @param {string} sender * @param {string} encrypted * @returns {string|Buffer} */ async decryptNext (sender:string, encrypted:string):Promise { return this.encryptor.decrypt( encrypted, await this.sessionKeyManager.getEncryptionKey(sender, true), await this.sessionKeyManager.getAssocData(sender) ) } /** * Sets the identity string for the current user. * * @param {string} id */ async setIdentityString (id:string):Promise { return this.identityKeyManager.setMyIdentityString(id) } } /* Let's make sure we export the interfaces/etc we use. */ export * from './src/symmetric' export * from './src/persistence' export * from './src/util'