parent
817cf5ed03
commit
a7d78c0e9b
38 changed files with 2996 additions and 789 deletions
|
@ -1,6 +1,5 @@
|
|||
/* global Signal: false */
|
||||
/* global Whisper: false */
|
||||
/* global dcodeIO: false */
|
||||
/* global _: false */
|
||||
/* global textsecure: false */
|
||||
/* global i18n: false */
|
||||
|
@ -48,7 +47,7 @@ function stringify(object) {
|
|||
object[key] = {
|
||||
type: 'ArrayBuffer',
|
||||
encoding: 'base64',
|
||||
data: dcodeIO.ByteBuffer.wrap(val).toString('base64'),
|
||||
data: crypto.arrayBufferToBase64(val),
|
||||
};
|
||||
} else if (val instanceof Object) {
|
||||
object[key] = stringify(val);
|
||||
|
@ -70,7 +69,7 @@ function unstringify(object) {
|
|||
val.encoding === 'base64' &&
|
||||
typeof val.data === 'string'
|
||||
) {
|
||||
object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer();
|
||||
object[key] = crypto.base64ToArrayBuffer(val.data);
|
||||
} else if (val instanceof Object) {
|
||||
object[key] = unstringify(object[key]);
|
||||
}
|
||||
|
|
|
@ -1,39 +1,82 @@
|
|||
/* eslint-env browser */
|
||||
/* global dcodeIO */
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable camelcase, no-bitwise */
|
||||
|
||||
module.exports = {
|
||||
encryptSymmetric,
|
||||
decryptSymmetric,
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
bytesFromString,
|
||||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesCtr,
|
||||
decryptSymmetric,
|
||||
deriveAccessKey,
|
||||
encryptAesCtr,
|
||||
encryptSymmetric,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
getAccessKeyVerifier,
|
||||
getRandomBytes,
|
||||
getViewOfArrayBuffer,
|
||||
getZeroes,
|
||||
highBitsToInt,
|
||||
hmacSha256,
|
||||
intsToByteHighAndLow,
|
||||
splitBytes,
|
||||
stringFromBytes,
|
||||
trimBytes,
|
||||
verifyAccessKey,
|
||||
};
|
||||
|
||||
// High-level Operations
|
||||
|
||||
async function deriveAccessKey(profileKey) {
|
||||
const iv = getZeroes(12);
|
||||
const plaintext = getZeroes(16);
|
||||
const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext);
|
||||
return _getFirstBytes(accessKey, 16);
|
||||
}
|
||||
|
||||
async function getAccessKeyVerifier(accessKey) {
|
||||
const plaintext = getZeroes(32);
|
||||
const hmac = await hmacSha256(accessKey, plaintext);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
async function verifyAccessKey(accessKey, theirVerifier) {
|
||||
const ourVerifier = await getAccessKeyVerifier(accessKey);
|
||||
|
||||
if (constantTimeEqual(ourVerifier, theirVerifier)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const IV_LENGTH = 16;
|
||||
const MAC_LENGTH = 16;
|
||||
const NONCE_LENGTH = 16;
|
||||
|
||||
async function encryptSymmetric(key, plaintext) {
|
||||
const iv = _getZeros(IV_LENGTH);
|
||||
const nonce = _getRandomBytes(NONCE_LENGTH);
|
||||
const iv = getZeroes(IV_LENGTH);
|
||||
const nonce = getRandomBytes(NONCE_LENGTH);
|
||||
|
||||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
||||
const cipherKey = await hmacSha256(key, nonce);
|
||||
const macKey = await hmacSha256(key, cipherKey);
|
||||
|
||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
|
||||
cipherKey,
|
||||
iv,
|
||||
plaintext
|
||||
);
|
||||
const mac = _getFirstBytes(
|
||||
await _hmac_SHA256(macKey, cipherText),
|
||||
MAC_LENGTH
|
||||
);
|
||||
const mac = _getFirstBytes(await hmacSha256(macKey, cipherText), MAC_LENGTH);
|
||||
|
||||
return _concatData([nonce, cipherText, mac]);
|
||||
return concatenateBytes(nonce, cipherText, mac);
|
||||
}
|
||||
|
||||
async function decryptSymmetric(key, data) {
|
||||
const iv = _getZeros(IV_LENGTH);
|
||||
const iv = getZeroes(IV_LENGTH);
|
||||
|
||||
const nonce = _getFirstBytes(data, NONCE_LENGTH);
|
||||
const cipherText = _getBytes(
|
||||
|
@ -43,11 +86,11 @@ async function decryptSymmetric(key, data) {
|
|||
);
|
||||
const theirMac = _getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH);
|
||||
|
||||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
||||
const cipherKey = await hmacSha256(key, nonce);
|
||||
const macKey = await hmacSha256(key, cipherKey);
|
||||
|
||||
const ourMac = _getFirstBytes(
|
||||
await _hmac_SHA256(macKey, cipherText),
|
||||
await hmacSha256(macKey, cipherText),
|
||||
MAC_LENGTH
|
||||
);
|
||||
if (!constantTimeEqual(theirMac, ourMac)) {
|
||||
|
@ -73,56 +116,135 @@ function constantTimeEqual(left, right) {
|
|||
return result === 0;
|
||||
}
|
||||
|
||||
async function _hmac_SHA256(key, data) {
|
||||
// Encryption
|
||||
|
||||
async function hmacSha256(key, plaintext) {
|
||||
const algorithm = {
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256',
|
||||
};
|
||||
const extractable = false;
|
||||
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
||||
algorithm,
|
||||
extractable,
|
||||
['sign']
|
||||
);
|
||||
|
||||
return window.crypto.subtle.sign(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
cryptoKey,
|
||||
data
|
||||
);
|
||||
return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext);
|
||||
}
|
||||
|
||||
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
||||
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
|
||||
const algorithm = {
|
||||
name: 'AES-CBC',
|
||||
iv,
|
||||
};
|
||||
const extractable = false;
|
||||
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{ name: 'AES-CBC' },
|
||||
algorithm,
|
||||
extractable,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
return window.crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
|
||||
return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
||||
}
|
||||
|
||||
async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
||||
async function _decrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
|
||||
const algorithm = {
|
||||
name: 'AES-CBC',
|
||||
iv,
|
||||
};
|
||||
const extractable = false;
|
||||
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{ name: 'AES-CBC' },
|
||||
algorithm,
|
||||
extractable,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
|
||||
return window.crypto.subtle.decrypt(algorithm, cryptoKey, plaintext);
|
||||
}
|
||||
|
||||
function _getRandomBytes(n) {
|
||||
async function encryptAesCtr(key, plaintext, counter) {
|
||||
const extractable = false;
|
||||
const algorithm = {
|
||||
name: 'AES-CTR',
|
||||
counter: new Uint8Array(counter),
|
||||
length: 128,
|
||||
};
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
algorithm,
|
||||
extractable,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
algorithm,
|
||||
cryptoKey,
|
||||
plaintext
|
||||
);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
async function decryptAesCtr(key, ciphertext, counter) {
|
||||
const extractable = false;
|
||||
const algorithm = {
|
||||
name: 'AES-CTR',
|
||||
counter: new Uint8Array(counter),
|
||||
length: 128,
|
||||
};
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
algorithm,
|
||||
extractable,
|
||||
['decrypt']
|
||||
);
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
algorithm,
|
||||
cryptoKey,
|
||||
ciphertext
|
||||
);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
async function _encrypt_aes_gcm(key, iv, plaintext) {
|
||||
const algorithm = {
|
||||
name: 'AES-GCM',
|
||||
iv,
|
||||
};
|
||||
const extractable = false;
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
algorithm,
|
||||
extractable,
|
||||
['encrypt']
|
||||
);
|
||||
return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
||||
}
|
||||
|
||||
// Utility
|
||||
|
||||
function getRandomBytes(n) {
|
||||
const bytes = new Uint8Array(n);
|
||||
window.crypto.getRandomValues(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function _getZeros(n) {
|
||||
function getZeroes(n) {
|
||||
const result = new Uint8Array(n);
|
||||
|
||||
const value = 0;
|
||||
|
@ -133,17 +255,43 @@ function _getZeros(n) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function _getFirstBytes(data, n) {
|
||||
const source = new Uint8Array(data);
|
||||
return source.subarray(0, n);
|
||||
function highBitsToInt(byte) {
|
||||
return (byte & 0xff) >> 4;
|
||||
}
|
||||
|
||||
function _getBytes(data, start, n) {
|
||||
const source = new Uint8Array(data);
|
||||
return source.subarray(start, start + n);
|
||||
function intsToByteHighAndLow(highValue, lowValue) {
|
||||
return ((highValue << 4) | lowValue) & 0xff;
|
||||
}
|
||||
|
||||
function _concatData(elements) {
|
||||
function trimBytes(buffer, length) {
|
||||
return _getFirstBytes(buffer, length);
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(arrayBuffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||
}
|
||||
function base64ToArrayBuffer(base64string) {
|
||||
return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
|
||||
}
|
||||
|
||||
function fromEncodedBinaryToArrayBuffer(key) {
|
||||
return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
|
||||
}
|
||||
|
||||
function bytesFromString(string) {
|
||||
return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
|
||||
}
|
||||
function stringFromBytes(buffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
||||
}
|
||||
|
||||
function getViewOfArrayBuffer(buffer, start, finish) {
|
||||
const source = new Uint8Array(buffer);
|
||||
const result = source.slice(start, finish);
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
function concatenateBytes(...elements) {
|
||||
const length = elements.reduce(
|
||||
(total, element) => total + element.byteLength,
|
||||
0
|
||||
|
@ -161,5 +309,45 @@ function _concatData(elements) {
|
|||
throw new Error('problem concatenating!');
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
function splitBytes(buffer, ...lengths) {
|
||||
const total = lengths.reduce((acc, length) => acc + length, 0);
|
||||
|
||||
if (total !== buffer.byteLength) {
|
||||
throw new Error(
|
||||
`Requested lengths total ${total} does not match source total ${
|
||||
buffer.byteLength
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const source = new Uint8Array(buffer);
|
||||
const results = [];
|
||||
let position = 0;
|
||||
|
||||
for (let i = 0, max = lengths.length; i < max; i += 1) {
|
||||
const length = lengths[i];
|
||||
const result = new Uint8Array(length);
|
||||
const section = source.slice(position, position + length);
|
||||
result.set(section);
|
||||
position += result.byteLength;
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Internal-only
|
||||
|
||||
function _getFirstBytes(data, n) {
|
||||
const source = new Uint8Array(data);
|
||||
return source.subarray(0, n);
|
||||
}
|
||||
|
||||
function _getBytes(data, start, n) {
|
||||
const source = new Uint8Array(data);
|
||||
return source.subarray(start, start + n);
|
||||
}
|
||||
|
|
13
js/modules/metadata/CiphertextMessage.js
Normal file
13
js/modules/metadata/CiphertextMessage.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
CURRENT_VERSION: 3,
|
||||
|
||||
// This matches Envelope.Type.CIPHERTEXT
|
||||
WHISPER_TYPE: 1,
|
||||
// This matches Envelope.Type.PREKEY_BUNDLE
|
||||
PREKEY_TYPE: 3,
|
||||
|
||||
SENDERKEY_TYPE: 4,
|
||||
SENDERKEY_DISTRIBUTION_TYPE: 5,
|
||||
|
||||
ENCRYPTED_MESSAGE_OVERHEAD: 53,
|
||||
};
|
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,
|
||||
};
|
120
js/modules/refresh_sender_certificate.js
Normal file
120
js/modules/refresh_sender_certificate.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/* global window, setTimeout, clearTimeout, textsecure, WebAPI, ConversationController */
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
};
|
||||
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000; // one day
|
||||
const MINIMUM_TIME_LEFT = 2 * 60 * 60 * 1000; // two hours
|
||||
|
||||
let initialized = false;
|
||||
let timeout = null;
|
||||
let scheduledTime = null;
|
||||
|
||||
// We need to refresh our own profile regularly to account for newly-added devices which
|
||||
// do not support unidentified delivery.
|
||||
function refreshOurProfile() {
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const conversation = ConversationController.getOrCreate(ourNumber, 'private');
|
||||
conversation.getProfiles();
|
||||
}
|
||||
|
||||
function initialize({ events, storage, navigator, logger }) {
|
||||
if (initialized) {
|
||||
logger.warn('refreshSenderCertificate: already initialized!');
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
runWhenOnline();
|
||||
|
||||
events.on('timetravel', () => {
|
||||
if (initialized) {
|
||||
scheduleNextRotation();
|
||||
}
|
||||
});
|
||||
|
||||
function scheduleNextRotation() {
|
||||
const now = Date.now();
|
||||
const certificate = storage.get('senderCertificate');
|
||||
if (!certificate) {
|
||||
setTimeoutForNextRun(now);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// The useful information in a SenderCertificate is all serialized, so we
|
||||
// need to do another layer of decoding.
|
||||
const decoded = textsecure.protobuf.SenderCertificate.Certificate.decode(
|
||||
certificate.certificate
|
||||
);
|
||||
const expires = decoded.expires.toNumber();
|
||||
|
||||
const time = Math.min(now + ONE_DAY, expires - MINIMUM_TIME_LEFT);
|
||||
|
||||
setTimeoutForNextRun(time);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
logger.info('refreshSenderCertificate: Getting new certificate...');
|
||||
try {
|
||||
const username = storage.get('number_id');
|
||||
const password = storage.get('password');
|
||||
const server = WebAPI.connect({ username, password });
|
||||
|
||||
const { certificate } = await server.getSenderCertificate();
|
||||
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate);
|
||||
const decoded = textsecure.protobuf.SenderCertificate.decode(arrayBuffer);
|
||||
|
||||
decoded.certificate = decoded.certificate.toArrayBuffer();
|
||||
decoded.signature = decoded.signature.toArrayBuffer();
|
||||
decoded.serialized = arrayBuffer;
|
||||
|
||||
storage.put('senderCertificate', decoded);
|
||||
scheduleNextRotation();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'refreshSenderCertificate: Get failed. Trying again in two minutes...',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
setTimeout(runWhenOnline, 2 * 60 * 1000);
|
||||
}
|
||||
|
||||
refreshOurProfile();
|
||||
}
|
||||
|
||||
function runWhenOnline() {
|
||||
if (navigator.onLine) {
|
||||
run();
|
||||
} else {
|
||||
logger.info(
|
||||
'refreshSenderCertificate: Offline. Will update certificate when online...'
|
||||
);
|
||||
const listener = () => {
|
||||
logger.info(
|
||||
'refreshSenderCertificate: Online. Now updating certificate...'
|
||||
);
|
||||
window.removeEventListener('online', listener);
|
||||
run();
|
||||
};
|
||||
window.addEventListener('online', listener);
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeoutForNextRun(time = Date.now()) {
|
||||
const now = Date.now();
|
||||
|
||||
if (scheduledTime !== time || !timeout) {
|
||||
logger.info(
|
||||
'Next sender certificate refresh scheduled for',
|
||||
new Date(time).toISOString()
|
||||
);
|
||||
}
|
||||
|
||||
scheduledTime = time;
|
||||
const waitTime = Math.max(0, time - now);
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(runWhenOnline, waitTime);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ const Settings = require('./settings');
|
|||
const Startup = require('./startup');
|
||||
const Util = require('../../ts/util');
|
||||
const { migrateToSQL } = require('./migrate_to_sql');
|
||||
const Metadata = require('./metadata/SecretSessionCipher');
|
||||
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
||||
|
||||
// Components
|
||||
const {
|
||||
|
@ -216,6 +218,7 @@ exports.setup = (options = {}) => {
|
|||
};
|
||||
|
||||
return {
|
||||
Metadata,
|
||||
Backbone,
|
||||
Components,
|
||||
Crypto,
|
||||
|
@ -225,6 +228,7 @@ exports.setup = (options = {}) => {
|
|||
Migrations,
|
||||
Notifications,
|
||||
OS,
|
||||
RefreshSenderCertificate,
|
||||
Settings,
|
||||
Startup,
|
||||
Types,
|
||||
|
|
|
@ -4,20 +4,28 @@ const Errors = require('./types/errors');
|
|||
const Settings = require('./settings');
|
||||
|
||||
exports.syncReadReceiptConfiguration = async ({
|
||||
ourNumber,
|
||||
deviceId,
|
||||
sendRequestConfigurationSyncMessage,
|
||||
storage,
|
||||
prepareForSend,
|
||||
}) => {
|
||||
if (!is.string(deviceId)) {
|
||||
throw new TypeError("'deviceId' is required");
|
||||
throw new TypeError('deviceId is required');
|
||||
}
|
||||
if (!is.function(sendRequestConfigurationSyncMessage)) {
|
||||
throw new TypeError('sendRequestConfigurationSyncMessage is required');
|
||||
}
|
||||
if (!is.function(prepareForSend)) {
|
||||
throw new TypeError('prepareForSend is required');
|
||||
}
|
||||
|
||||
if (!is.function(sendRequestConfigurationSyncMessage)) {
|
||||
throw new TypeError("'sendRequestConfigurationSyncMessage' is required");
|
||||
if (!is.string(ourNumber)) {
|
||||
throw new TypeError('ourNumber is required');
|
||||
}
|
||||
|
||||
if (!is.object(storage)) {
|
||||
throw new TypeError("'storage' is required");
|
||||
throw new TypeError('storage is required');
|
||||
}
|
||||
|
||||
const isPrimaryDevice = deviceId === '1';
|
||||
|
@ -38,7 +46,8 @@ exports.syncReadReceiptConfiguration = async ({
|
|||
}
|
||||
|
||||
try {
|
||||
await sendRequestConfigurationSyncMessage();
|
||||
const { wrap, sendOptions } = prepareForSend(ourNumber);
|
||||
await wrap(sendRequestConfigurationSyncMessage(sendOptions));
|
||||
storage.put(settingName, true);
|
||||
} catch (error) {
|
||||
return {
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
/* global dcodeIO, crypto */
|
||||
/* global crypto */
|
||||
|
||||
const { isFunction, isNumber } = require('lodash');
|
||||
const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
|
||||
const { arrayBufferToBase64, base64ToArrayBuffer } = require('../crypto');
|
||||
|
||||
async function computeHash(arraybuffer) {
|
||||
const hash = await crypto.subtle.digest({ name: 'SHA-512' }, arraybuffer);
|
||||
return arrayBufferToBase64(hash);
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(arraybuffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(arraybuffer).toString('base64');
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(base64) {
|
||||
return dcodeIO.ByteBuffer.wrap(base64, 'base64').toArrayBuffer();
|
||||
}
|
||||
|
||||
function buildAvatarUpdater({ field }) {
|
||||
return async (conversation, data, options = {}) => {
|
||||
if (!conversation) {
|
||||
|
|
|
@ -80,6 +80,8 @@ function _ensureStringed(thing) {
|
|||
return res;
|
||||
} else if (thing === null) {
|
||||
return null;
|
||||
} else if (thing === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
throw new Error(`unsure of how to jsonify object of type ${typeof thing}`);
|
||||
}
|
||||
|
@ -160,7 +162,9 @@ function _createSocket(url, { certificateAuthority, proxyUrl }) {
|
|||
function _promiseAjax(providedUrl, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = providedUrl || `${options.host}/${options.path}`;
|
||||
log.info(options.type, url);
|
||||
log.info(
|
||||
`${options.type} ${url}${options.unauthenticated ? ' (unauth)' : ''}`
|
||||
);
|
||||
const timeout =
|
||||
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
|
||||
|
||||
|
@ -188,15 +192,26 @@ function _promiseAjax(providedUrl, options) {
|
|||
fetchOptions.headers['Content-Length'] = contentLength;
|
||||
}
|
||||
|
||||
if (options.user && options.password) {
|
||||
const { accessKey, unauthenticated } = options;
|
||||
if (unauthenticated) {
|
||||
if (!accessKey) {
|
||||
throw new Error(
|
||||
'_promiseAjax: mode is aunathenticated, but accessKey was not provided'
|
||||
);
|
||||
}
|
||||
// Access key is already a Base64 string
|
||||
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
|
||||
} else if (options.user && options.password) {
|
||||
const user = _getString(options.user);
|
||||
const password = _getString(options.password);
|
||||
const auth = _btoa(`${user}:${password}`);
|
||||
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
||||
}
|
||||
|
||||
if (options.contentType) {
|
||||
fetchOptions.headers['Content-Type'] = options.contentType;
|
||||
}
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then(response => {
|
||||
let resultPromise;
|
||||
|
@ -292,12 +307,14 @@ function HTTPError(message, providedCode, response, stack) {
|
|||
|
||||
const URL_CALLS = {
|
||||
accounts: 'v1/accounts',
|
||||
attachment: 'v1/attachments',
|
||||
deliveryCert: 'v1/certificate/delivery',
|
||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||
devices: 'v1/devices',
|
||||
keys: 'v2/keys',
|
||||
signed: 'v2/keys/signed',
|
||||
messages: 'v1/messages',
|
||||
attachment: 'v1/attachments',
|
||||
profile: 'v1/profile',
|
||||
signed: 'v2/keys/signed',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -335,16 +352,21 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
getAttachment,
|
||||
getAvatar,
|
||||
getDevices,
|
||||
getSenderCertificate,
|
||||
registerSupportForUnauthenticatedDelivery,
|
||||
getKeysForNumber,
|
||||
getKeysForNumberUnauth,
|
||||
getMessageSocket,
|
||||
getMyKeys,
|
||||
getProfile,
|
||||
getProfileUnauth,
|
||||
getProvisioningSocket,
|
||||
putAttachment,
|
||||
registerKeys,
|
||||
requestVerificationSMS,
|
||||
requestVerificationVoice,
|
||||
sendMessages,
|
||||
sendMessagesUnauth,
|
||||
setSignedPreKey,
|
||||
};
|
||||
|
||||
|
@ -366,6 +388,8 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
type: param.httpType,
|
||||
user: username,
|
||||
validateResponse: param.validateResponse,
|
||||
unauthenticated: param.unauthenticated,
|
||||
accessKey: param.accessKey,
|
||||
}).catch(e => {
|
||||
const { code } = e;
|
||||
if (code === 200) {
|
||||
|
@ -405,6 +429,23 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
});
|
||||
}
|
||||
|
||||
function getSenderCertificate() {
|
||||
return _ajax({
|
||||
call: 'deliveryCert',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
schema: { certificate: 'string' },
|
||||
});
|
||||
}
|
||||
|
||||
function registerSupportForUnauthenticatedDelivery() {
|
||||
return _ajax({
|
||||
call: 'supportUnauthenticatedDelivery',
|
||||
httpType: 'PUT',
|
||||
responseType: 'json',
|
||||
});
|
||||
}
|
||||
|
||||
function getProfile(number) {
|
||||
return _ajax({
|
||||
call: 'profile',
|
||||
|
@ -413,6 +454,16 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
responseType: 'json',
|
||||
});
|
||||
}
|
||||
function getProfileUnauth(number, { accessKey } = {}) {
|
||||
return _ajax({
|
||||
call: 'profile',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}`,
|
||||
responseType: 'json',
|
||||
unauthenticated: true,
|
||||
accessKey,
|
||||
});
|
||||
}
|
||||
|
||||
function getAvatar(path) {
|
||||
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
|
||||
|
@ -449,13 +500,19 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
newPassword,
|
||||
signalingKey,
|
||||
registrationId,
|
||||
deviceName
|
||||
deviceName,
|
||||
options = {}
|
||||
) {
|
||||
const { accessKey } = options;
|
||||
const jsonData = {
|
||||
signalingKey: _btoa(_getString(signalingKey)),
|
||||
supportsSms: false,
|
||||
fetchesMessages: true,
|
||||
registrationId,
|
||||
unidentifiedAccessKey: accessKey
|
||||
? _btoa(_getString(accessKey))
|
||||
: undefined,
|
||||
unrestrictedUnidentifiedAccess: false,
|
||||
};
|
||||
|
||||
let call;
|
||||
|
@ -552,6 +609,43 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
}).then(res => res.count);
|
||||
}
|
||||
|
||||
function handleKeys(res) {
|
||||
if (!Array.isArray(res.devices)) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
res.identityKey = _base64ToBytes(res.identityKey);
|
||||
res.devices.forEach(device => {
|
||||
if (
|
||||
!_validateResponse(device, { signedPreKey: 'object' }) ||
|
||||
!_validateResponse(device.signedPreKey, {
|
||||
publicKey: 'string',
|
||||
signature: 'string',
|
||||
})
|
||||
) {
|
||||
throw new Error('Invalid signedPreKey');
|
||||
}
|
||||
if (device.preKey) {
|
||||
if (
|
||||
!_validateResponse(device, { preKey: 'object' }) ||
|
||||
!_validateResponse(device.preKey, { publicKey: 'string' })
|
||||
) {
|
||||
throw new Error('Invalid preKey');
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.preKey.publicKey = _base64ToBytes(device.preKey.publicKey);
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.publicKey = _base64ToBytes(
|
||||
device.signedPreKey.publicKey
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.signature = _base64ToBytes(
|
||||
device.signedPreKey.signature
|
||||
);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
function getKeysForNumber(number, deviceId = '*') {
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
|
@ -559,41 +653,46 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
urlParameters: `/${number}/${deviceId}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
}).then(res => {
|
||||
if (res.devices.constructor !== Array) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
res.identityKey = _base64ToBytes(res.identityKey);
|
||||
res.devices.forEach(device => {
|
||||
if (
|
||||
!_validateResponse(device, { signedPreKey: 'object' }) ||
|
||||
!_validateResponse(device.signedPreKey, {
|
||||
publicKey: 'string',
|
||||
signature: 'string',
|
||||
})
|
||||
) {
|
||||
throw new Error('Invalid signedPreKey');
|
||||
}
|
||||
if (device.preKey) {
|
||||
if (
|
||||
!_validateResponse(device, { preKey: 'object' }) ||
|
||||
!_validateResponse(device.preKey, { publicKey: 'string' })
|
||||
) {
|
||||
throw new Error('Invalid preKey');
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.preKey.publicKey = _base64ToBytes(device.preKey.publicKey);
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.publicKey = _base64ToBytes(
|
||||
device.signedPreKey.publicKey
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.signature = _base64ToBytes(
|
||||
device.signedPreKey.signature
|
||||
);
|
||||
});
|
||||
return res;
|
||||
}).then(handleKeys);
|
||||
}
|
||||
|
||||
function getKeysForNumberUnauth(
|
||||
number,
|
||||
deviceId = '*',
|
||||
{ accessKey } = {}
|
||||
) {
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}/${deviceId}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
unauthenticated: true,
|
||||
accessKey,
|
||||
}).then(handleKeys);
|
||||
}
|
||||
|
||||
function sendMessagesUnauth(
|
||||
destination,
|
||||
messageArray,
|
||||
timestamp,
|
||||
silent,
|
||||
{ accessKey } = {}
|
||||
) {
|
||||
const jsonData = { messages: messageArray, timestamp };
|
||||
|
||||
if (silent) {
|
||||
jsonData.silent = true;
|
||||
}
|
||||
|
||||
return _ajax({
|
||||
call: 'messages',
|
||||
httpType: 'PUT',
|
||||
urlParameters: `/${destination}`,
|
||||
jsonData,
|
||||
responseType: 'json',
|
||||
unauthenticated: true,
|
||||
accessKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue