-
-
Save KyrneDev/f919175b63205c98651e450f9d9cbdce to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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