/**
* 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 {
CryptographyKey,
Ed25519PublicKey,
Ed25519SecretKey,
SodiumPlus,
X25519PublicKey, X25519SecretKey
} from "sodium-plus";
import {
KeyDerivationFunction,
blakeKdf,
SymmetricEncryptionInterface,
SymmetricCrypto
} from "./lib/symmetric";
import {
DefaultSessionKeyManager,
SessionKeyManagerInterface,
IdentityKeyManagerInterface,
DefaultIdentityKeyManager
} from "./lib/persistence";
import {
concat,
generateKeyPair,
generateBundle,
signBundle,
verifyBundle,
wipe
} from "./lib/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 = [];
for (let 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) {
let 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: 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) {
let 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: 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) {
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 "./lib/symmetric";
export * from "./lib/persistence";
export * from "./lib/util";