Skip to content

Instantly share code, notes, and snippets.

@KyrneDev
Last active June 2, 2020 23:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KyrneDev/f919175b63205c98651e450f9d9cbdce to your computer and use it in GitHub Desktop.
Save KyrneDev/f919175b63205c98651e450f9d9cbdce to your computer and use it in GitHub Desktop.
/**
* Copyright (c) 2020, Kyrne, All rights reserved.
*/
import {__decorate} from 'tslib';
const SIGN_ALGORITHM_NAME = "ECDSA";
const DH_ALGORITHM_NAME = "ECDH";
const SECRET_KEY_NAME = "AES-CBC";
const HASH_NAME = "SHA-256";
const HMAC_NAME = "HMAC";
const MAX_RATCHET_STACK_SIZE = 30;
const INFO_TEXT = Convert.FromBinary("InfoText");
const INFO_RATCHET = Convert.FromBinary("InfoRatchet");
const INFO_MESSAGE_KEYS = Convert.FromBinary("InfoMessageKeys");
let engine = null;
if (typeof self !== "undefined") {
engine = {
crypto: self.crypto,
name: "WebCrypto",
};
}
function setEngine(name, crypto) {
engine = {
crypto,
name,
};
}
function getEngine() {
return engine;
}
class Curve {
static async generateKeyPair(type) {
const name = type;
const usage = type === "ECDSA" ? ["sign", "verify"] : ["deriveKey", "deriveBits"];
const keys = await getEngine().crypto.subtle.generateKey({name, namedCurve: this.NAMED_CURVE}, true, usage);
const publicKey = await ECPublicKey.create(keys.publicKey);
const res = {
privateKey: keys.privateKey,
publicKey,
};
return res;
}
static deriveBytes(privateKey, publicKey) {
return getEngine().crypto.subtle.deriveBits({name: "ECDH", public: publicKey.key}, privateKey, 256);
}
static verify(signingKey, message, signature) {
return getEngine().crypto.subtle
.verify({name: "ECDSA", hash: this.DIGEST_ALGORITHM}, signingKey.key, signature, message);
}
static async sign(signingKey, message) {
return getEngine().crypto.subtle.sign({name: "ECDSA", hash: this.DIGEST_ALGORITHM}, signingKey, message);
}
static async ecKeyPairToJson(key) {
let thumbprint;
if (typeof key.publicKey.thumbprint === 'function') {
thumbprint = await key.publicKey.thumbprint();
} else {
thumbprint = key.publicKey.id;
}
const privKey = await window.crypto.subtle.exportKey(
'jwk',
key.privateKey
)
const pubKey = await window.crypto.subtle.exportKey(
'jwk',
key.publicKey.key
)
return {
privateKey: privKey,
publicKey: pubKey,
thumbprint,
}
}
static async ecKeyPairFromJson(keys) {
const type = (keys.privateKey['key_ops'][0] === 'sign' ? 'ECDSA' : 'ECDH')
const privKey = await window.crypto.subtle.importKey(
'jwk',
keys.privateKey,
{name: type, namedCurve: Curve.NAMED_CURVE},
true,
keys.privateKey['key_ops']
)
const pubKey = await window.crypto.subtle.importKey(
'jwk',
keys.publicKey,
{name: type, namedCurve: Curve.NAMED_CURVE},
true,
keys.publicKey['key_ops']
)
return {
privateKey: privKey,
publicKey: await ECPublicKey.create(pubKey),
};
}
}
Curve.NAMED_CURVE = "P-256";
Curve.DIGEST_ALGORITHM = "SHA-512";
const AES_ALGORITHM = {name: "AES-CBC", length: 256};
class Secret {
static randomBytes(size) {
const array = new Uint8Array(size);
getEngine().crypto.getRandomValues(array);
return array.buffer;
}
static digest(alg, message) {
return getEngine().crypto.subtle.digest(alg, message);
}
static encrypt(key, data, iv) {
return getEngine().crypto.subtle.encrypt({name: SECRET_KEY_NAME, iv: new Uint8Array(iv)}, key, data);
}
static decrypt(key, data, iv) {
return getEngine().crypto.subtle.decrypt({name: SECRET_KEY_NAME, iv: new Uint8Array(iv)}, key, data);
}
static importHMAC(raw) {
return getEngine().crypto.subtle
.importKey("raw", raw, {name: HMAC_NAME, hash: {name: HASH_NAME}}, true, ["sign", "verify"]);
}
static importAES(raw) {
return getEngine().crypto.subtle.importKey("raw", raw, AES_ALGORITHM, false, ["encrypt", "decrypt"]);
}
static async sign(key, data) {
return await getEngine().crypto.subtle.sign({name: HMAC_NAME, hash: HASH_NAME}, key, data);
}
static async HKDF(IKM, keysCount = 1, salt, info = new ArrayBuffer(0)) {
if (!salt) {
salt = await this.importHMAC(new Uint8Array(32).buffer);
}
const PRKBytes = await this.sign(salt, IKM);
const infoBuffer = new ArrayBuffer(32 + info.byteLength + 1);
const PRK = await this.importHMAC(PRKBytes);
const T = [new ArrayBuffer(0)];
for (let i = 0; i < keysCount; i++) {
T[i + 1] = await this.sign(PRK, combine(T[i], info, new Uint8Array([i + 1]).buffer));
}
return T.slice(1);
}
}
class ECPublicKey {
static async create(publicKey) {
const res = new this();
const algName = publicKey.algorithm.name.toUpperCase();
if (!(algName === "ECDH" || algName === "ECDSA")) {
throw new Error("Error: Unsupported asymmetric key algorithm.");
}
if (publicKey.type !== "public") {
throw new Error("Error: Expected key type to be public but it was not.");
}
res.key = publicKey;
const jwk = await getEngine().crypto.subtle.exportKey("jwk", publicKey);
if (!(jwk.x && jwk.y)) {
throw new Error("Wrong JWK data for EC public key. Parameters x and y are required.");
}
const x = Convert.FromBase64Url(jwk.x);
const y = Convert.FromBase64Url(jwk.y);
const xy = Convert.ToBinary(x) + Convert.ToBinary(y);
res.serialized = Convert.FromBinary(xy);
res.id = await res.thumbprint();
return res;
}
static async importKey(bytes, type) {
const x = Convert.ToBase64Url(bytes.slice(0, 32));
const y = Convert.ToBase64Url(bytes.slice(32));
const jwk = {
crv: Curve.NAMED_CURVE,
kty: "EC",
x,
y,
};
const usage = (type === "ECDSA" ? ["verify"] : []);
const key = await getEngine().crypto.subtle
.importKey("jwk", jwk, {name: type, namedCurve: Curve.NAMED_CURVE}, true, usage);
const res = await ECPublicKey.create(key);
return res;
}
serialize() {
return this.serialized;
}
async thumbprint() {
const bytes = await this.serialize();
const thumbprint = await Secret.digest("SHA-256", bytes);
return Convert.ToHex(thumbprint);
}
async isEqual(other) {
if (!(other && other instanceof ECPublicKey)) {
return false;
}
return isEqual(this.serialized, other.serialized);
}
}
class Identity {
constructor(id, signingKey, exchangeKey) {
this.id = id;
this.signingKey = signingKey;
this.exchangeKey = exchangeKey;
this.preKeys = [];
this.signedPreKeys = [];
}
static async fromJSON(obj) {
const signingKey = await Curve.ecKeyPairFromJson(obj.signingKey);
const exchangeKey = await Curve.ecKeyPairFromJson(obj.exchangeKey);
const res = new this(obj.id, signingKey, exchangeKey);
res.createdAt = new Date(obj.createdAt);
await res.fromJSON(obj);
return res;
}
static async create(id, signedPreKeyAmount = 0, preKeyAmount = 0) {
const signingKey = await Curve.generateKeyPair(SIGN_ALGORITHM_NAME);
const exchangeKey = await Curve.generateKeyPair(DH_ALGORITHM_NAME);
const res = new Identity(id, signingKey, exchangeKey);
res.createdAt = new Date();
for (let i = 0; i < preKeyAmount; i++) {
res.preKeys.push(await Curve.generateKeyPair("ECDH"));
}
for (let i = 0; i < signedPreKeyAmount; i++) {
res.signedPreKeys.push(await Curve.generateKeyPair("ECDH"));
}
return res;
}
async toJSON() {
const preKeys = [];
const signedPreKeys = [];
for (const key of this.preKeys) {
preKeys.push(await Curve.ecKeyPairToJson(key));
}
for (const key of this.signedPreKeys) {
signedPreKeys.push(await Curve.ecKeyPairToJson(key));
}
return {
createdAt: this.createdAt.toISOString(),
exchangeKey: await Curve.ecKeyPairToJson(this.exchangeKey),
id: this.id,
preKeys,
signedPreKeys,
signingKey: await Curve.ecKeyPairToJson(this.signingKey),
};
}
async fromJSON(obj) {
this.id = obj.id;
this.signingKey = await Curve.ecKeyPairFromJson(obj.signingKey);
this.exchangeKey = await Curve.ecKeyPairFromJson(obj.exchangeKey);
this.preKeys = [];
for (const key of obj.preKeys) {
this.preKeys.push(await Curve.ecKeyPairFromJson(key));
}
this.signedPreKeys = [];
for (const key of obj.signedPreKeys) {
this.signedPreKeys.push(await Curve.ecKeyPairFromJson(key));
}
}
}
class RemoteIdentity {
static fill(protocol) {
const res = new RemoteIdentity();
res.fill(protocol);
return res;
}
static async fromJSON(obj) {
const res = new this();
await res.fromJSON(obj);
return res;
}
fill(protocol) {
this.signingKey = protocol.signingKey;
this.exchangeKey = protocol.exchangeKey;
this.signature = protocol.signature;
this.createdAt = protocol.createdAt;
}
verify() {
let sig = this.signature;
if (typeof this.signature === 'object') {
}
return Curve.verify(this.signingKey, this.exchangeKey.serialize(), sig);
}
async toJSON() {
let signingKey = this.signingKey.key;
if (this.signingKey.key.extractable) {
signingKey = await window.crypto.subtle.exportKey('jwk', this.signingKey.key)
}
return {
createdAt: this.createdAt.toISOString(),
exchangeKey: await window.crypto.subtle.exportKey('jwk', this.exchangeKey.key),
id: this.signingKey.id,
signature: this.signature,
signingKey,
thumbprint: await this.signingKey.thumbprint(),
};
}
async fromJSON(obj) {
this.id = obj.id;
this.signature = new Uint8Array(Object.values(obj.signature));
this.signingKey = await ECPublicKey.create(await window.crypto.subtle.importKey('jwk', obj.signingKey, {
name: SIGN_ALGORITHM_NAME,
namedCurve: Curve.NAMED_CURVE
}, true, ['verify']));
this.exchangeKey = await ECPublicKey.create(await window.crypto.subtle.importKey('jwk', obj.exchangeKey, {
name: DH_ALGORITHM_NAME,
namedCurve: Curve.NAMED_CURVE
}, true, []));
this.createdAt = new Date(obj.createdAt);
const ok = await this.verify();
if (!ok) {
throw new Error("Error: Wrong signature for RemoteIdentity");
}
}
}
let BaseProtocol = class BaseProtocol extends ObjectProto {
};
__decorate([
ProtobufProperty({id: 0, type: "uint32", defaultValue: 1})
], BaseProtocol.prototype, "version", void 0);
BaseProtocol = __decorate([
ProtobufElement({name: "Base"})
], BaseProtocol);
class ECDSAPublicKeyConverter {
static async set(value) {
return new Uint8Array(value.serialized);
}
static async get(value) {
return ECPublicKey.importKey(value.buffer, "ECDSA");
}
}
class ECDHPublicKeyConverter {
static async set(value) {
return new Uint8Array(value.serialized);
}
static async get(value) {
return ECPublicKey.importKey(value.buffer, "ECDH");
}
}
class DateConverter {
static async set(value) {
return new Uint8Array(Convert.FromString(value.toISOString()));
}
static async get(value) {
return new Date(Convert.ToString(value));
}
}
var IdentityProtocol_1;
let IdentityProtocol = IdentityProtocol_1 = class IdentityProtocol extends BaseProtocol {
static async fill(identity) {
const res = new IdentityProtocol_1();
await res.fill(identity);
return res;
}
async sign(key) {
this.signature = await Curve.sign(key, this.exchangeKey.serialized);
}
async verify() {
return await Curve.verify(this.signingKey, this.exchangeKey.serialize(), this.signature);
}
async fill(identity) {
this.signingKey = identity.signingKey.publicKey;
this.exchangeKey = identity.exchangeKey.publicKey;
this.createdAt = identity.createdAt;
await this.sign(identity.signingKey.privateKey);
}
};
__decorate([
ProtobufProperty({id: 1, converter: ECDSAPublicKeyConverter})
], IdentityProtocol.prototype, "signingKey", void 0);
__decorate([
ProtobufProperty({id: 2, converter: ECDHPublicKeyConverter})
], IdentityProtocol.prototype, "exchangeKey", void 0);
__decorate([
ProtobufProperty({id: 3})
], IdentityProtocol.prototype, "signature", void 0);
__decorate([
ProtobufProperty({id: 4, converter: DateConverter})
], IdentityProtocol.prototype, "createdAt", void 0);
IdentityProtocol = IdentityProtocol_1 = __decorate([
ProtobufElement({name: "Identity"})
], IdentityProtocol);
let MessageProtocol = class MessageProtocol extends BaseProtocol {
};
__decorate([
ProtobufProperty({id: 1, converter: ECDHPublicKeyConverter, required: true})
], MessageProtocol.prototype, "senderRatchetKey", void 0);
__decorate([
ProtobufProperty({id: 2, type: "uint32", required: true})
], MessageProtocol.prototype, "counter", void 0);
__decorate([
ProtobufProperty({id: 3, type: "uint32", required: true})
], MessageProtocol.prototype, "previousCounter", void 0);
__decorate([
ProtobufProperty({id: 4, converter: ArrayBufferConverter, required: true})
], MessageProtocol.prototype, "cipherText", void 0);
MessageProtocol = __decorate([
ProtobufElement({name: "Message"})
], MessageProtocol);
let MessageSignedProtocol = class MessageSignedProtocol extends BaseProtocol {
async sign(hmacKey) {
this.signature = await this.signHMAC(hmacKey);
}
async verify(hmacKey) {
const signature = await this.signHMAC(hmacKey);
return isEqual(signature, this.signature);
}
async getSignedRaw() {
const receiverKey = this.receiverKey.serialized;
const senderKey = this.senderKey.serialized;
const message = await this.message.exportProto();
const data = combine(receiverKey, senderKey, message);
return data;
}
async signHMAC(macKey) {
const data = await this.getSignedRaw();
const signature = await Secret.sign(macKey, data);
return signature;
}
};
__decorate([
ProtobufProperty({id: 1, converter: ECDSAPublicKeyConverter, required: true})
], MessageSignedProtocol.prototype, "senderKey", void 0);
__decorate([
ProtobufProperty({id: 2, parser: MessageProtocol, required: true})
], MessageSignedProtocol.prototype, "message", void 0);
__decorate([
ProtobufProperty({id: 3, required: true})
], MessageSignedProtocol.prototype, "signature", void 0);
MessageSignedProtocol = __decorate([
ProtobufElement({name: "MessageSigned"})
], MessageSignedProtocol);
let PreKeyMessageProtocol = class PreKeyMessageProtocol extends BaseProtocol {
};
__decorate([
ProtobufProperty({id: 1, type: "uint32", required: true})
], PreKeyMessageProtocol.prototype, "registrationId", void 0);
__decorate([
ProtobufProperty({id: 2, type: "uint32"})
], PreKeyMessageProtocol.prototype, "preKeyId", void 0);
__decorate([
ProtobufProperty({id: 3, type: "uint32", required: true})
], PreKeyMessageProtocol.prototype, "preKeySignedId", void 0);
__decorate([
ProtobufProperty({id: 4, converter: ECDHPublicKeyConverter, required: true})
], PreKeyMessageProtocol.prototype, "baseKey", void 0);
__decorate([
ProtobufProperty({id: 5, parser: IdentityProtocol, required: true})
], PreKeyMessageProtocol.prototype, "identity", void 0);
__decorate([
ProtobufProperty({id: 6, parser: MessageSignedProtocol, required: true})
], PreKeyMessageProtocol.prototype, "signedMessage", void 0);
PreKeyMessageProtocol = __decorate([
ProtobufElement({name: "PreKeyMessage"})
], PreKeyMessageProtocol);
let PreKeyProtocol = class PreKeyProtocol extends BaseProtocol {
};
__decorate([
ProtobufProperty({id: 1, type: "uint32", required: true})
], PreKeyProtocol.prototype, "id", void 0);
__decorate([
ProtobufProperty({id: 2, converter: ECDHPublicKeyConverter, required: true})
], PreKeyProtocol.prototype, "key", void 0);
PreKeyProtocol = __decorate([
ProtobufElement({name: "PreKey"})
], PreKeyProtocol);
let PreKeySignedProtocol = class PreKeySignedProtocol extends PreKeyProtocol {
async sign(key) {
this.signature = await Curve.sign(key, this.key.serialize());
}
verify(key) {
return Curve.verify(key, this.key.serialize(), this.signature);
}
};
__decorate([
ProtobufProperty({id: 3, converter: ArrayBufferConverter, required: true})
], PreKeySignedProtocol.prototype, "signature", void 0);
PreKeySignedProtocol = __decorate([
ProtobufElement({name: "PreKeySigned"})
], PreKeySignedProtocol);
let PreKeyBundleProtocol = class PreKeyBundleProtocol extends BaseProtocol {
};
__decorate([
ProtobufProperty({id: 1, type: "uint32", required: true})
], PreKeyBundleProtocol.prototype, "registrationId", void 0);
__decorate([
ProtobufProperty({id: 2, parser: IdentityProtocol, required: true})
], PreKeyBundleProtocol.prototype, "identity", void 0);
__decorate([
ProtobufProperty({id: 3, parser: PreKeyProtocol})
], PreKeyBundleProtocol.prototype, "preKey", void 0);
__decorate([
ProtobufProperty({id: 4, parser: PreKeySignedProtocol, required: true})
], PreKeyBundleProtocol.prototype, "preKeySigned", void 0);
PreKeyBundleProtocol = __decorate([
ProtobufElement({name: "PreKeyBundle"})
], PreKeyBundleProtocol);
class Stack {
constructor(maxSize = 20) {
this.items = [];
this.maxSize = maxSize;
}
get length() {
return this.items.length;
}
get latest() {
return this.items[this.length - 1];
}
push(item) {
if (this.length === this.maxSize) {
this.items = this.items.slice(1);
}
this.items.push(item);
}
async toJSON() {
const res = [];
for (const item of this.items) {
res.push(await item.toJSON());
}
return res;
}
async fromJSON(obj) {
this.items = obj;
}
}
const CIPHER_KEY_KDF_INPUT = new Uint8Array([1]).buffer;
const ROOT_KEY_KDF_INPUT = new Uint8Array([2]).buffer;
class SymmetricRatchet {
constructor(rootKey) {
this.counter = 0;
this.previousCounter = 0;
this.rootKey = rootKey;
}
static async fromJSON(obj) {
const res = new this(obj.rootKey);
res.fromJSON(obj);
return res;
}
async toJSON() {
return {
counter: this.counter,
rootKey: this.rootKey,
};
}
async fromJSON(obj) {
this.counter = obj.counter;
this.rootKey = obj.rootKey;
}
async calculateKey(rootKey) {
const cipherKeyBytes = await Secret.sign(rootKey, CIPHER_KEY_KDF_INPUT);
const nextRootKeyBytes = await Secret.sign(rootKey, ROOT_KEY_KDF_INPUT);
const res = {
cipher: cipherKeyBytes,
rootKey: await Secret.importHMAC(nextRootKeyBytes),
};
return res;
}
async click() {
const rootKey = this.rootKey;
const res = await this.calculateKey(rootKey);
this.rootKey = res.rootKey;
this.counter++;
return res.cipher;
}
}
class SendingRatchet extends SymmetricRatchet {
async encrypt(message) {
const cipherKey = await this.click();
const keys = await Secret.HKDF(cipherKey, 3, void 0, INFO_MESSAGE_KEYS);
const aesKey = await Secret.importAES(keys[0]);
const hmacKey = await Secret.importHMAC(keys[1]);
const iv = keys[2].slice(0, 16);
const cipherText = await Secret.encrypt(aesKey, message, iv);
return {
cipherText,
hmacKey,
};
}
}
class ReceivingRatchet extends SymmetricRatchet {
constructor() {
super(...arguments);
this.keys = [];
}
async toJSON() {
const res = (await super.toJSON());
res.keys = this.keys;
return res;
}
async fromJSON(obj) {
await super.fromJSON(obj);
this.keys = obj.keys;
}
async decrypt(message, counter) {
const cipherKey = await this.getKey(counter);
const keys = await Secret.HKDF(cipherKey, 3, void 0, INFO_MESSAGE_KEYS);
const aesKey = await Secret.importAES(keys[0]);
const hmacKey = await Secret.importHMAC(keys[1]);
const iv = keys[2].slice(0, 16);
const cipherText = await Secret.decrypt(aesKey, message, iv);
return {
cipherText,
hmacKey,
};
}
async getKey(counter) {
while (this.counter <= counter) {
const cipherKey = await this.click();
this.keys.push(cipherKey);
}
const key = this.keys[counter];
return key;
}
}
async function authenticateA(IKa, EKa, IKb, SPKb, OPKb) {
const DH1 = await Curve.deriveBytes(IKa.exchangeKey.privateKey, SPKb);
const DH2 = await Curve.deriveBytes(EKa.privateKey, IKb);
const DH3 = await Curve.deriveBytes(EKa.privateKey, SPKb);
let DH4 = new ArrayBuffer(0);
if (OPKb) {
DH4 = await Curve.deriveBytes(EKa.privateKey, OPKb);
}
const _F = new Uint8Array(32);
for (let i = 0; i < _F.length; i++) {
_F[i] = 0xff;
}
const F = _F.buffer;
const KM = combine(F, DH1, DH2, DH3, DH4);
const keys = await Secret.HKDF(KM, 1, void 0, INFO_TEXT);
return await Secret.importHMAC(keys[0]);
}
async function authenticateB(IKb, SPKb, IKa, EKa, OPKb) {
const DH1 = await Curve.deriveBytes(SPKb.privateKey, IKa);
const DH2 = await Curve.deriveBytes(IKb.exchangeKey.privateKey, EKa);
const DH3 = await Curve.deriveBytes(SPKb.privateKey, EKa);
let DH4 = new ArrayBuffer(0);
if (OPKb) {
DH4 = await Curve.deriveBytes(OPKb, EKa);
}
const _F = new Uint8Array(32);
for (let i = 0; i < _F.length; i++) {
_F[i] = 0xff;
}
const F = _F.buffer;
const KM = combine(F, DH1, DH2, DH3, DH4);
const keys = await Secret.HKDF(KM, 1, void 0, INFO_TEXT);
return await Secret.importHMAC(keys[0]);
}
class AsymmetricRatchet extends EventEmitter {
constructor() {
super();
this.counter = 0;
this.remoteCounter = 0;
this.currentStep = new DHRatchetStep();
this.steps = new DHRatchetStepStack(MAX_RATCHET_STACK_SIZE);
this.promises = {};
}
static async create(identity, protocol) {
let rootKey;
const ratchet = new AsymmetricRatchet();
if (protocol instanceof PreKeyBundleProtocol) {
if (!await protocol.identity.verify()) {
throw new Error("Error: Remote client's identity key is invalid.");
}
if (!await protocol.preKeySigned.verify(protocol.identity.signingKey)) {
throw new Error("Error: Remote client's signed prekey is invalid.");
}
ratchet.currentRatchetKey = await ratchet.generateRatchetKey();
ratchet.currentStep.remoteRatchetKey = protocol.preKeySigned.key;
ratchet.remoteIdentity = RemoteIdentity.fill(protocol.identity);
ratchet.remoteIdentity.id = protocol.registrationId;
ratchet.remotePreKeyId = protocol.preKey.id;
ratchet.remotePreKeySignedId = protocol.preKeySigned.id;
rootKey = await authenticateA(identity, ratchet.currentRatchetKey, protocol.identity.exchangeKey, protocol.preKeySigned.key, protocol.preKey.key);
}
else {
if (!await protocol.identity.verify()) {
throw new Error("Error: Remote client's identity key is invalid.");
}
const signedPreKey = identity.signedPreKeys[protocol.preKeySignedId];
if (!signedPreKey) {
throw new Error(`Error: PreKey with id ${protocol.preKeySignedId} not found`);
}
let preKey;
if (protocol.preKeyId !== void 0) {
preKey = identity.preKeys[protocol.preKeyId];
}
ratchet.remoteIdentity = RemoteIdentity.fill(protocol.identity);
ratchet.currentRatchetKey = signedPreKey;
rootKey = await authenticateB(identity, ratchet.currentRatchetKey, protocol.identity.exchangeKey, protocol.signedMessage.message.senderRatchetKey, preKey && preKey.privateKey);
}
ratchet.identity = identity;
ratchet.id = identity.id;
ratchet.rootKey = rootKey;
return ratchet;
}
static async fromJSON(identity, remote, obj) {
const res = new AsymmetricRatchet();
res.identity = identity;
res.remoteIdentity = remote;
await res.fromJSON(obj);
return res;
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
async decrypt(protocol) {
return this.queuePromise("encrypt", async () => {
const remoteRatchetKey = protocol.message.senderRatchetKey;
const message = protocol.message;
this.remoteCounter = protocol.message.previousCounter;
if (this.remoteCounter > this.counter - 15) {
this.counter = this.remoteCounter;
}
if (protocol.message.previousCounter < this.counter - MAX_RATCHET_STACK_SIZE) {
window.location.reload();
throw new Error("Error: Too old message");
}
let step = this.steps.getStep(remoteRatchetKey);
if (!step) {
const ratchetStep = new DHRatchetStep();
ratchetStep.remoteRatchetKey = remoteRatchetKey;
this.steps.push(ratchetStep);
this.currentStep = ratchetStep;
step = ratchetStep;
}
if (!step.receivingChain) {
step.receivingChain = await this.createChain(this.currentRatchetKey.privateKey, remoteRatchetKey, ReceivingRatchet);
}
const decryptedMessage = await step.receivingChain.decrypt(message.cipherText, message.counter);
this.update();
protocol.senderKey = this.remoteIdentity.signingKey;
protocol.receiverKey = this.identity.signingKey.publicKey;
if (!await protocol.verify(decryptedMessage.hmacKey)) {
throw new Error("Error: The Message did not successfully verify!");
}
step.receivingChain.counter++;
return decryptedMessage.cipherText;
});
}
async encrypt(message) {
return this.queuePromise("encrypt", async () => {
if (this.currentStep.receivingChain && !this.currentStep.sendingChain) {
this.counter++;
this.currentRatchetKey = await this.generateRatchetKey();
}
if (!this.currentStep.sendingChain) {
if (!this.currentStep.remoteRatchetKey) {
throw new Error("currentStep has empty remoteRatchetKey");
}
this.currentStep.sendingChain = await this.createChain(this.currentRatchetKey.privateKey, this.currentStep.remoteRatchetKey, SendingRatchet);
}
const encryptedMessage = await this.currentStep.sendingChain.encrypt(message);
this.update();
let preKeyMessage;
if (this.steps.length === 0 &&
!this.currentStep.receivingChain &&
this.currentStep.sendingChain.counter === 1) {
preKeyMessage = new PreKeyMessageProtocol();
preKeyMessage.registrationId = this.identity.id;
preKeyMessage.preKeyId = this.remotePreKeyId;
preKeyMessage.preKeySignedId = this.remotePreKeySignedId;
preKeyMessage.baseKey = this.currentRatchetKey.publicKey;
await preKeyMessage.identity.fill(this.identity);
}
const signedMessage = new MessageSignedProtocol();
signedMessage.receiverKey = this.remoteIdentity.signingKey;
signedMessage.senderKey = this.identity.signingKey.publicKey;
signedMessage.message.cipherText = encryptedMessage.cipherText;
signedMessage.message.counter = this.currentStep.sendingChain.counter - 1;
signedMessage.message.previousCounter = this.counter;
signedMessage.message.senderRatchetKey = this.currentRatchetKey.publicKey;
await signedMessage.sign(encryptedMessage.hmacKey);
delete this.currentStep.sendingChain;
if (preKeyMessage) {
preKeyMessage.signedMessage = signedMessage;
return preKeyMessage;
}
else {
return signedMessage;
}
});
}
async hasRatchetKey(key) {
let ecKey;
if (!(key instanceof ECPublicKey)) {
ecKey = await ECPublicKey.create(key);
}
else {
ecKey = key;
}
for (const item of this.steps.items) {
if (await item.remoteRatchetKey.isEqual(ecKey)) {
return true;
}
}
return false;
}
async toJSON() {
return {
counter: this.counter,
ratchetKey: await Curve.ecKeyPairToJson(this.currentRatchetKey),
remoteIdentity: await this.remoteIdentity.toJSON(),
rootKey: this.rootKey,
steps: await this.steps.toJSON(),
};
}
async fromJSON(obj) {
this.currentRatchetKey = await Curve.ecKeyPairFromJson(obj.ratchetKey);
this.counter = obj.counter;
this.rootKey = obj.rootKey;
this.remoteIdentity = await RemoteIdentity.fromJSON(this.remoteIdentity);
for (const step of obj.steps) {
this.currentStep = await DHRatchetStep.fromJSON(step);
this.steps.push(this.currentStep);
}
}
update() {
this.emit("update");
}
generateRatchetKey() {
return Curve.generateKeyPair("ECDH");
}
async createChain(ourRatchetKey, theirRatchetKey, ratchetClass) {
const derivedBytes = await Curve.deriveBytes(ourRatchetKey, theirRatchetKey);
const keys = await Secret.HKDF(derivedBytes, 2, this.rootKey, INFO_RATCHET);
const rootKey = await Secret.importHMAC(keys[0]);
const chainKey = await Secret.importHMAC(keys[1]);
const chain = new ratchetClass(chainKey);
this.rootKey = rootKey;
return chain;
}
queuePromise(key, fn) {
const prev = this.promises[key] || Promise.resolve();
const cur = this.promises[key] = prev.then(fn, fn);
cur.then(() => {
if (this.promises[key] === cur) {
delete this.promises[key];
}
});
return cur;
}
}
class DHRatchetStep {
static async fromJSON(obj) {
const res = new this();
await res.fromJSON(obj);
return res;
}
async toJSON() {
const res = {};
if (this.remoteRatchetKey) {
res.remoteRatchetKey = await window.crypto.subtle.exportKey('jwk', this.remoteRatchetKey.key);
}
if (this.sendingChain) {
res.sendingChain = await this.sendingChain.toJSON();
}
if (this.receivingChain) {
res.receivingChain = await this.receivingChain.toJSON();
}
return res;
}
async fromJSON(obj) {
if (obj.remoteRatchetKey) {
this.remoteRatchetKey = await ECPublicKey.create(await window.crypto.subtle.importKey('jwk', obj.remoteRatchetKey, {
name: DH_ALGORITHM_NAME,
namedCurve: Curve.NAMED_CURVE
}, true, []));
}
if (obj.sendingChain) {
this.sendingChain = await SendingRatchet.fromJSON(obj.sendingChain);
}
if (obj.receivingChain) {
this.receivingChain = await ReceivingRatchet.fromJSON(obj.receivingChain);
}
}
}
class DHRatchetStepStack extends Stack {
getStep(remoteRatchetKey) {
let found;
this.items.some((step) => {
if (step.remoteRatchetKey.id === remoteRatchetKey.id) {
found = step;
}
return !!found;
});
return found;
}
}
export {
AsymmetricRatchet,
Curve,
ECPublicKey,
Identity,
IdentityProtocol,
MessageSignedProtocol,
PreKeyBundleProtocol,
PreKeyMessageProtocol,
RemoteIdentity,
Secret,
getEngine,
setEngine
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment