/**
* 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'