Sealed Sender support

https://signal.org/blog/sealed-sender/
This commit is contained in:
Scott Nonnenberg 2018-10-17 18:01:21 -07:00
parent 817cf5ed03
commit a7d78c0e9b
38 changed files with 2996 additions and 789 deletions

View file

@ -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,
});
}