parent
817cf5ed03
commit
a7d78c0e9b
38 changed files with 2996 additions and 789 deletions
586
js/modules/metadata/SecretSessionCipher.js
Normal file
586
js/modules/metadata/SecretSessionCipher.js
Normal file
|
@ -0,0 +1,586 @@
|
|||
/* global libsignal, textsecure */
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
const CiphertextMessage = require('./CiphertextMessage');
|
||||
const {
|
||||
bytesFromString,
|
||||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesCtr,
|
||||
encryptAesCtr,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
getViewOfArrayBuffer,
|
||||
getZeroes,
|
||||
highBitsToInt,
|
||||
hmacSha256,
|
||||
intsToByteHighAndLow,
|
||||
splitBytes,
|
||||
trimBytes,
|
||||
} = require('../crypto');
|
||||
|
||||
const REVOKED_CERTIFICATES = [];
|
||||
|
||||
function SecretSessionCipher(storage) {
|
||||
this.storage = storage;
|
||||
|
||||
// We do this on construction because libsignal won't be available when this file loads
|
||||
const { SessionCipher } = libsignal;
|
||||
this.SessionCipher = SessionCipher;
|
||||
}
|
||||
|
||||
const CIPHERTEXT_VERSION = 1;
|
||||
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery';
|
||||
|
||||
// public CertificateValidator(ECPublicKey trustRoot)
|
||||
function createCertificateValidator(trustRoot) {
|
||||
return {
|
||||
// public void validate(SenderCertificate certificate, long validationTime)
|
||||
async validate(certificate, validationTime) {
|
||||
const serverCertificate = certificate.signer;
|
||||
|
||||
await libsignal.Curve.async.verifySignature(
|
||||
trustRoot,
|
||||
serverCertificate.certificate,
|
||||
serverCertificate.signature
|
||||
);
|
||||
|
||||
const serverCertId = serverCertificate.certificate.id;
|
||||
if (REVOKED_CERTIFICATES.includes(serverCertId)) {
|
||||
throw new Error(
|
||||
`Server certificate id ${serverCertId} has been revoked`
|
||||
);
|
||||
}
|
||||
|
||||
await libsignal.Curve.async.verifySignature(
|
||||
serverCertificate.key,
|
||||
certificate.certificate,
|
||||
certificate.signature
|
||||
);
|
||||
|
||||
if (validationTime > certificate.expires) {
|
||||
throw new Error('Certificate is expired');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function _decodePoint(serialized, offset = 0) {
|
||||
const view =
|
||||
offset > 0
|
||||
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
|
||||
: serialized;
|
||||
|
||||
return libsignal.Curve.validatePubKeyFormat(view);
|
||||
}
|
||||
|
||||
// public ServerCertificate(byte[] serialized)
|
||||
function _createServerCertificateFromBuffer(serialized) {
|
||||
const wrapper = textsecure.protobuf.ServerCertificate.decode(serialized);
|
||||
|
||||
if (!wrapper.certificate || !wrapper.signature) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
const certificate = textsecure.protobuf.ServerCertificate.Certificate.decode(
|
||||
wrapper.certificate.toArrayBuffer()
|
||||
);
|
||||
|
||||
if (!certificate.id || !certificate.key) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
id: certificate.id,
|
||||
key: certificate.key.toArrayBuffer(),
|
||||
serialized,
|
||||
certificate: wrapper.certificate.toArrayBuffer(),
|
||||
|
||||
signature: wrapper.signature.toArrayBuffer(),
|
||||
};
|
||||
}
|
||||
|
||||
// public SenderCertificate(byte[] serialized)
|
||||
function _createSenderCertificateFromBuffer(serialized) {
|
||||
const wrapper = textsecure.protobuf.SenderCertificate.decode(serialized);
|
||||
|
||||
if (!wrapper.signature || !wrapper.certificate) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
const certificate = textsecure.protobuf.SenderCertificate.Certificate.decode(
|
||||
wrapper.certificate.toArrayBuffer()
|
||||
);
|
||||
|
||||
if (
|
||||
!certificate.signer ||
|
||||
!certificate.identityKey ||
|
||||
!certificate.senderDevice ||
|
||||
!certificate.expires ||
|
||||
!certificate.sender
|
||||
) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
sender: certificate.sender,
|
||||
senderDevice: certificate.senderDevice,
|
||||
expires: certificate.expires.toNumber(),
|
||||
identityKey: certificate.identityKey.toArrayBuffer(),
|
||||
signer: _createServerCertificateFromBuffer(
|
||||
certificate.signer.toArrayBuffer()
|
||||
),
|
||||
|
||||
certificate: wrapper.certificate.toArrayBuffer(),
|
||||
signature: wrapper.signature.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessage(byte[] serialized)
|
||||
function _createUnidentifiedSenderMessageFromBuffer(serialized) {
|
||||
const version = highBitsToInt(serialized[0]);
|
||||
|
||||
if (version > CIPHERTEXT_VERSION) {
|
||||
throw new Error(`Unknown version: ${this.version}`);
|
||||
}
|
||||
|
||||
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
|
||||
const unidentifiedSenderMessage = textsecure.protobuf.UnidentifiedSenderMessage.decode(
|
||||
view
|
||||
);
|
||||
|
||||
if (
|
||||
!unidentifiedSenderMessage.ephemeralPublic ||
|
||||
!unidentifiedSenderMessage.encryptedStatic ||
|
||||
!unidentifiedSenderMessage.encryptedMessage
|
||||
) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
|
||||
ephemeralPublic: unidentifiedSenderMessage.ephemeralPublic.toArrayBuffer(),
|
||||
encryptedStatic: unidentifiedSenderMessage.encryptedStatic.toArrayBuffer(),
|
||||
encryptedMessage: unidentifiedSenderMessage.encryptedMessage.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessage(
|
||||
// ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) {
|
||||
function _createUnidentifiedSenderMessage(
|
||||
ephemeralPublic,
|
||||
encryptedStatic,
|
||||
encryptedMessage
|
||||
) {
|
||||
const versionBytes = new Uint8Array([
|
||||
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
|
||||
]);
|
||||
const unidentifiedSenderMessage = new textsecure.protobuf.UnidentifiedSenderMessage();
|
||||
|
||||
unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
|
||||
unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
|
||||
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
|
||||
|
||||
const messageBytes = unidentifiedSenderMessage.encode().toArrayBuffer();
|
||||
|
||||
return {
|
||||
version: CIPHERTEXT_VERSION,
|
||||
|
||||
ephemeralPublic,
|
||||
encryptedStatic,
|
||||
encryptedMessage,
|
||||
|
||||
serialized: concatenateBytes(versionBytes, messageBytes),
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessageContent(byte[] serialized)
|
||||
function _createUnidentifiedSenderMessageContentFromBuffer(serialized) {
|
||||
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||
|
||||
const message = textsecure.protobuf.UnidentifiedSenderMessage.Message.decode(
|
||||
serialized
|
||||
);
|
||||
|
||||
if (!message.type || !message.senderCertificate || !message.content) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
let type;
|
||||
switch (message.type) {
|
||||
case TypeEnum.MESSAGE:
|
||||
type = CiphertextMessage.WHISPER_TYPE;
|
||||
break;
|
||||
case TypeEnum.PREKEY_MESSAGE:
|
||||
type = CiphertextMessage.PREKEY_TYPE;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown type: ${message.type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
senderCertificate: _createSenderCertificateFromBuffer(
|
||||
message.senderCertificate.toArrayBuffer()
|
||||
),
|
||||
content: message.content.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// private int getProtoType(int type)
|
||||
function _getProtoMessageType(type) {
|
||||
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||
|
||||
switch (type) {
|
||||
case CiphertextMessage.WHISPER_TYPE:
|
||||
return TypeEnum.MESSAGE;
|
||||
case CiphertextMessage.PREKEY_TYPE:
|
||||
return TypeEnum.PREKEY_MESSAGE;
|
||||
default:
|
||||
throw new Error(`_getProtoMessageType: type '${type}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessageContent(
|
||||
// int type, SenderCertificate senderCertificate, byte[] content)
|
||||
function _createUnidentifiedSenderMessageContent(
|
||||
type,
|
||||
senderCertificate,
|
||||
content
|
||||
) {
|
||||
const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message();
|
||||
innerMessage.type = _getProtoMessageType(type);
|
||||
innerMessage.senderCertificate = textsecure.protobuf.SenderCertificate.decode(
|
||||
senderCertificate.serialized
|
||||
);
|
||||
innerMessage.content = content;
|
||||
|
||||
return {
|
||||
type,
|
||||
senderCertificate,
|
||||
content,
|
||||
|
||||
serialized: innerMessage.encode().toArrayBuffer(),
|
||||
};
|
||||
}
|
||||
|
||||
SecretSessionCipher.prototype = {
|
||||
// public byte[] encrypt(
|
||||
// SignalProtocolAddress destinationAddress,
|
||||
// SenderCertificate senderCertificate,
|
||||
// byte[] paddedPlaintext
|
||||
// )
|
||||
async encrypt(destinationAddress, senderCertificate, paddedPlaintext) {
|
||||
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||
const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this);
|
||||
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||
|
||||
const sessionCipher = new SessionCipher(
|
||||
signalProtocolStore,
|
||||
destinationAddress
|
||||
);
|
||||
const sessionRecord = await sessionCipher.getRecord(
|
||||
destinationAddress.toString()
|
||||
);
|
||||
const openSession = sessionRecord.getOpenSession();
|
||||
if (!openSession) {
|
||||
throw new Error('No active session');
|
||||
}
|
||||
|
||||
const message = await sessionCipher.encrypt(paddedPlaintext);
|
||||
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
||||
const theirIdentity = fromEncodedBinaryToArrayBuffer(
|
||||
openSession.indexInfo.remoteIdentityKey
|
||||
);
|
||||
|
||||
const ephemeral = await libsignal.Curve.async.generateKeyPair();
|
||||
const ephemeralSalt = concatenateBytes(
|
||||
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||
theirIdentity,
|
||||
ephemeral.pubKey
|
||||
);
|
||||
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||
theirIdentity,
|
||||
ephemeral.privKey,
|
||||
ephemeralSalt
|
||||
);
|
||||
const staticKeyCiphertext = await _encryptWithSecretKeys(
|
||||
ephemeralKeys.cipherKey,
|
||||
ephemeralKeys.macKey,
|
||||
ourIdentity.pubKey
|
||||
);
|
||||
|
||||
const staticSalt = concatenateBytes(
|
||||
ephemeralKeys.chainKey,
|
||||
staticKeyCiphertext
|
||||
);
|
||||
const staticKeys = await _calculateStaticKeys(
|
||||
theirIdentity,
|
||||
ourIdentity.privKey,
|
||||
staticSalt
|
||||
);
|
||||
const content = _createUnidentifiedSenderMessageContent(
|
||||
message.type,
|
||||
senderCertificate,
|
||||
fromEncodedBinaryToArrayBuffer(message.body)
|
||||
);
|
||||
const messageBytes = await _encryptWithSecretKeys(
|
||||
staticKeys.cipherKey,
|
||||
staticKeys.macKey,
|
||||
content.serialized
|
||||
);
|
||||
|
||||
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
|
||||
ephemeral.pubKey,
|
||||
staticKeyCiphertext,
|
||||
messageBytes
|
||||
);
|
||||
|
||||
return unidentifiedSenderMessage.serialized;
|
||||
},
|
||||
|
||||
// public Pair<SignalProtocolAddress, byte[]> decrypt(
|
||||
// CertificateValidator validator, byte[] ciphertext, long timestamp)
|
||||
async decrypt(validator, ciphertext, timestamp, me) {
|
||||
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||
const signalProtocolStore = this.storage;
|
||||
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||
const _decryptWithUnidentifiedSenderMessage = this._decryptWithUnidentifiedSenderMessage.bind(
|
||||
this
|
||||
);
|
||||
const _decryptWithSecretKeys = this._decryptWithSecretKeys.bind(this);
|
||||
|
||||
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
||||
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
|
||||
const ephemeralSalt = concatenateBytes(
|
||||
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||
ourIdentity.pubKey,
|
||||
wrapper.ephemeralPublic
|
||||
);
|
||||
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||
wrapper.ephemeralPublic,
|
||||
ourIdentity.privKey,
|
||||
ephemeralSalt
|
||||
);
|
||||
const staticKeyBytes = await _decryptWithSecretKeys(
|
||||
ephemeralKeys.cipherKey,
|
||||
ephemeralKeys.macKey,
|
||||
wrapper.encryptedStatic
|
||||
);
|
||||
|
||||
const staticKey = _decodePoint(staticKeyBytes, 0);
|
||||
const staticSalt = concatenateBytes(
|
||||
ephemeralKeys.chainKey,
|
||||
wrapper.encryptedStatic
|
||||
);
|
||||
const staticKeys = await _calculateStaticKeys(
|
||||
staticKey,
|
||||
ourIdentity.privKey,
|
||||
staticSalt
|
||||
);
|
||||
const messageBytes = await _decryptWithSecretKeys(
|
||||
staticKeys.cipherKey,
|
||||
staticKeys.macKey,
|
||||
wrapper.encryptedMessage
|
||||
);
|
||||
|
||||
const content = _createUnidentifiedSenderMessageContentFromBuffer(
|
||||
messageBytes
|
||||
);
|
||||
|
||||
await validator.validate(content.senderCertificate, timestamp);
|
||||
if (
|
||||
!constantTimeEqual(content.senderCertificate.identityKey, staticKeyBytes)
|
||||
) {
|
||||
throw new Error(
|
||||
"Sender's certificate key does not match key used in message"
|
||||
);
|
||||
}
|
||||
|
||||
const { sender, senderDevice } = content.senderCertificate;
|
||||
const { number, deviceId } = me || {};
|
||||
if (sender === number && senderDevice === deviceId) {
|
||||
return {
|
||||
isMe: true,
|
||||
};
|
||||
}
|
||||
const address = new libsignal.SignalProtocolAddress(sender, senderDevice);
|
||||
|
||||
try {
|
||||
return {
|
||||
sender: address,
|
||||
content: await _decryptWithUnidentifiedSenderMessage(content),
|
||||
};
|
||||
} catch (error) {
|
||||
error.sender = address;
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// public int getSessionVersion(SignalProtocolAddress remoteAddress) {
|
||||
getSessionVersion(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.getSessionVersion();
|
||||
},
|
||||
|
||||
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
|
||||
getRemoteRegistrationId(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.getRemoteRegistrationId();
|
||||
},
|
||||
|
||||
// Used by outgoing_message.js
|
||||
closeOpenSessionForDevice(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.closeOpenSessionForDevice();
|
||||
},
|
||||
|
||||
// private EphemeralKeys calculateEphemeralKeys(
|
||||
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
|
||||
async _calculateEphemeralKeys(ephemeralPublic, ephemeralPrivate, salt) {
|
||||
const ephemeralSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
ephemeralPublic,
|
||||
ephemeralPrivate
|
||||
);
|
||||
const ephemeralDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||
ephemeralSecret,
|
||||
salt,
|
||||
new ArrayBuffer()
|
||||
);
|
||||
|
||||
// private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey)
|
||||
return {
|
||||
chainKey: ephemeralDerivedParts[0],
|
||||
cipherKey: ephemeralDerivedParts[1],
|
||||
macKey: ephemeralDerivedParts[2],
|
||||
};
|
||||
},
|
||||
|
||||
// private StaticKeys calculateStaticKeys(
|
||||
// ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt)
|
||||
async _calculateStaticKeys(staticPublic, staticPrivate, salt) {
|
||||
const staticSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
staticPublic,
|
||||
staticPrivate
|
||||
);
|
||||
const staticDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||
staticSecret,
|
||||
salt,
|
||||
new ArrayBuffer()
|
||||
);
|
||||
|
||||
// private StaticKeys(byte[] cipherKey, byte[] macKey)
|
||||
return {
|
||||
cipherKey: staticDerivedParts[1],
|
||||
macKey: staticDerivedParts[2],
|
||||
};
|
||||
},
|
||||
|
||||
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
|
||||
_decryptWithUnidentifiedSenderMessage(message) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const sender = new libsignal.SignalProtocolAddress(
|
||||
message.senderCertificate.sender,
|
||||
message.senderCertificate.senderDevice
|
||||
);
|
||||
|
||||
switch (message.type) {
|
||||
case CiphertextMessage.WHISPER_TYPE:
|
||||
return new SessionCipher(
|
||||
signalProtocolStore,
|
||||
sender
|
||||
).decryptWhisperMessage(message.content);
|
||||
case CiphertextMessage.PREKEY_TYPE:
|
||||
return new SessionCipher(
|
||||
signalProtocolStore,
|
||||
sender
|
||||
).decryptPreKeyWhisperMessage(message.content);
|
||||
default:
|
||||
throw new Error(`Unknown type: ${message.type}`);
|
||||
}
|
||||
},
|
||||
|
||||
// private byte[] encrypt(
|
||||
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
|
||||
async _encryptWithSecretKeys(cipherKey, macKey, plaintext) {
|
||||
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||
// cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||
|
||||
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||
// mac.init(macKey);
|
||||
|
||||
// byte[] const ciphertext = cipher.doFinal(plaintext);
|
||||
const ciphertext = await encryptAesCtr(cipherKey, plaintext, getZeroes(16));
|
||||
|
||||
// byte[] const ourFullMac = mac.doFinal(ciphertext);
|
||||
const ourFullMac = await hmacSha256(macKey, ciphertext);
|
||||
const ourMac = trimBytes(ourFullMac, 10);
|
||||
|
||||
return concatenateBytes(ciphertext, ourMac);
|
||||
},
|
||||
|
||||
// private byte[] decrypt(
|
||||
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext)
|
||||
async _decryptWithSecretKeys(cipherKey, macKey, ciphertext) {
|
||||
if (ciphertext.byteLength < 10) {
|
||||
throw new Error('Ciphertext not long enough for MAC!');
|
||||
}
|
||||
|
||||
const ciphertextParts = splitBytes(
|
||||
ciphertext,
|
||||
ciphertext.byteLength - 10,
|
||||
10
|
||||
);
|
||||
|
||||
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||
// mac.init(macKey);
|
||||
|
||||
// byte[] const digest = mac.doFinal(ciphertextParts[0]);
|
||||
const digest = await hmacSha256(macKey, ciphertextParts[0]);
|
||||
const ourMac = trimBytes(digest, 10);
|
||||
const theirMac = ciphertextParts[1];
|
||||
|
||||
if (!constantTimeEqual(ourMac, theirMac)) {
|
||||
throw new Error('Bad mac!');
|
||||
}
|
||||
|
||||
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||
// cipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||
|
||||
// return cipher.doFinal(ciphertextParts[0]);
|
||||
return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16));
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
SecretSessionCipher,
|
||||
createCertificateValidator,
|
||||
_createServerCertificateFromBuffer,
|
||||
_createSenderCertificateFromBuffer,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue