Skip to content
25 changes: 22 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ interface E2ESession {
};
}

export interface ILogger {
info(obj?: any, msg?: string): void;
warn(obj?: any, msg?: string): void;
error(obj?: any, msg?: string): void;
debug(obj?: any, msg?: string): void;
trace(obj?: any, msg?: string): void;
}

export interface SignalStorage {
loadSession(id: string): Promise<SessionRecord | null | undefined>;
storeSession(id: string, session: SessionRecord): Promise<void>;
Expand All @@ -30,26 +38,37 @@ export interface SignalStorage {
}

export class ProtocolAddress {
public readonly id: string;
public readonly deviceId: number;

constructor(name: string, deviceId: number);
public getName(): string;
public getDeviceId(): number;
public toString(): string;
}

export class SessionRecord {
static deserialize(serialized: Uint8Array): SessionRecord;
static deserialize(serialized: Uint8Array, logger?: ILogger): SessionRecord;
public serialize(): Uint8Array;
public haveOpenSession(): boolean;
}

export class SessionCipher {
constructor(storage: SignalStorage, remoteAddress: ProtocolAddress);
constructor(
storage: SignalStorage,
remoteAddress: ProtocolAddress,
logger?: ILogger
);
public decryptPreKeyWhisperMessage(ciphertext: Uint8Array): Promise<Buffer>;
public decryptWhisperMessage(ciphertext: Uint8Array): Promise<Buffer>;
public encrypt(data: Uint8Array): Promise<{ type: number; body: string }>;
}

export class SessionBuilder {
constructor(storage: SignalStorage, remoteAddress: ProtocolAddress);
constructor(
storage: SignalStorage,
remoteAddress: ProtocolAddress,
logger?: ILogger
);
public initOutgoing(session: E2ESession): Promise<void>;
}
8 changes: 6 additions & 2 deletions src/curve.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ILogger } from '../index';

export interface KeyPairType {
pubKey: Uint8Array;
privKey: Uint8Array;
Expand All @@ -7,7 +9,8 @@ export function generateKeyPair(): KeyPairType;

export function calculateAgreement(
publicKey: Uint8Array,
privateKey: Uint8Array
privateKey: Uint8Array,
logger?: ILogger
): Uint8Array;

export function calculateSignature(
Expand All @@ -18,5 +21,6 @@ export function calculateSignature(
export function verifySignature(
publicKey: Uint8Array,
message: Uint8Array,
signature: Uint8Array
signature: Uint8Array,
logger?: ILogger
): boolean;
14 changes: 7 additions & 7 deletions src/curve.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

'use strict';

const curveJs = require('curve25519-js');
const nodeCrypto = require('crypto');
const noopLogger = require('./noop_logger');
// from: https://github.com/digitalbazaar/x25519-key-agreement-key-2019/blob/master/lib/crypto.js
const PUBLIC_KEY_DER_PREFIX = Buffer.from([
48, 42, 48, 5, 6, 3, 43, 101, 110, 3, 33, 0
Expand Down Expand Up @@ -30,7 +30,7 @@ function validatePrivKey(privKey) {
}
}

function scrubPubKeyFormat(pubKey) {
function scrubPubKeyFormat(pubKey, logger = noopLogger) {
if (!(pubKey instanceof Buffer)) {
throw new Error(`Invalid public key type: ${pubKey.constructor.name}`);
}
Expand All @@ -40,7 +40,7 @@ function scrubPubKeyFormat(pubKey) {
if (pubKey.byteLength == 33) {
return pubKey.slice(1);
} else {
console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
logger.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
return pubKey;
}
}
Expand Down Expand Up @@ -90,8 +90,8 @@ exports.generateKeyPair = function() {
}
};

exports.calculateAgreement = function(pubKey, privKey) {
pubKey = scrubPubKeyFormat(pubKey);
exports.calculateAgreement = function(pubKey, privKey, logger = noopLogger) {
pubKey = scrubPubKeyFormat(pubKey, logger);
validatePrivKey(privKey);
if (!pubKey || pubKey.byteLength != 32) {
throw new Error("Invalid public key");
Expand Down Expand Up @@ -127,8 +127,8 @@ exports.calculateSignature = function(privKey, message) {
return Buffer.from(curveJs.sign(privKey, message));
};

exports.verifySignature = function(pubKey, msg, sig, isInit) {
pubKey = scrubPubKeyFormat(pubKey);
exports.verifySignature = function(pubKey, msg, sig, isInit, logger = noopLogger) {
pubKey = scrubPubKeyFormat(pubKey, logger);
if (!pubKey || pubKey.byteLength != 32) {
throw new Error("Invalid public key");
}
Expand Down
11 changes: 11 additions & 0 deletions src/noop_logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// vim: ts=4:sw=4:expandtab

const noopLogger = {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
trace: () => {},
};

module.exports = noopLogger;
5 changes: 3 additions & 2 deletions src/queue_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
'use strict';

const noopLogger = require('./noop_logger');

const _queueAsyncBuckets = new Map();
const _gcLimit = 10000;
Expand Down Expand Up @@ -37,7 +38,7 @@ async function _asyncQueueExecutor(queue, cleanup) {
cleanup();
}

module.exports = function(bucket, awaitable) {
module.exports = function(bucket, awaitable, logger = noopLogger) {
/* Run the async awaitable only when all other async calls registered
* here have completed (or thrown). The bucket argument is a hashable
* key representing the task queue to use. */
Expand All @@ -47,7 +48,7 @@ module.exports = function(bucket, awaitable) {
if (typeof bucket === 'string') {
awaitable.name = bucket;
} else {
console.warn("Unhandled bucket type (for naming):", typeof bucket, bucket);
logger.warn("Unhandled bucket type (for naming):", typeof bucket, bucket);
}
}
let inactive;
Expand Down
15 changes: 8 additions & 7 deletions src/session_builder.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

'use strict';

const BaseKeyType = require('./base_key_type');
Expand All @@ -8,13 +7,15 @@ const crypto = require('./crypto');
const curve = require('./curve');
const errors = require('./errors');
const queueJob = require('./queue_job');
const noopLogger = require('./noop_logger');


class SessionBuilder {

constructor(storage, protocolAddress) {
constructor(storage, protocolAddress, logger) {
this.addr = protocolAddress;
this.storage = storage;
this.logger = logger || noopLogger;
}

async initOutgoing(device) {
Expand Down Expand Up @@ -43,13 +44,13 @@ class SessionBuilder {
} else {
const openSession = record.getOpenSession();
if (openSession) {
console.warn("Closing stale open session for new outgoing prekey bundle");
record.closeSession(openSession);
this.logger.warn("Closing stale open session for new outgoing prekey bundle");
record.closeSession(openSession, this.logger);
}
}
record.setSession(session);
await this.storage.storeSession(fqAddr, record);
});
}, this.logger);
}

async initIncoming(record, message) {
Expand All @@ -71,8 +72,8 @@ class SessionBuilder {
}
const existingOpenSession = record.getOpenSession();
if (existingOpenSession) {
console.warn("Closing open session in favor of incoming prekey bundle");
record.closeSession(existingOpenSession);
this.logger.warn("Closing open session in favor of incoming prekey bundle");
record.closeSession(existingOpenSession, this.logger);
}
record.setSession(await this.initSession(false, preKeyPair, signedPreKeyPair,
message.identityKey, message.baseKey,
Expand Down
18 changes: 10 additions & 8 deletions src/session_cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const curve = require('./curve');
const errors = require('./errors');
const protobufs = require('./protobufs');
const queueJob = require('./queue_job');
const noopLogger = require('./noop_logger');

const VERSION = 3;

Expand All @@ -22,12 +23,13 @@ function assertBuffer(value) {

class SessionCipher {

constructor(storage, protocolAddress) {
constructor(storage, protocolAddress, logger) {
if (!(protocolAddress instanceof ProtocolAddress)) {
throw new TypeError("protocolAddress must be a ProtocolAddress");
}
this.addr = protocolAddress;
this.storage = storage;
this.logger = logger || noopLogger;
}

_encodeTupleByte(number1, number2) {
Expand All @@ -54,12 +56,12 @@ class SessionCipher {
}

async storeRecord(record) {
record.removeOldSessions();
record.removeOldSessions(this.logger);
await this.storage.storeSession(this.addr.toString(), record);
}

async queueJob(awaitable) {
return await queueJob(this.addr.toString(), awaitable);
return await queueJob(this.addr.toString(), awaitable, this.logger);
}

async encrypt(data) {
Expand Down Expand Up @@ -154,9 +156,9 @@ class SessionCipher {
errs.push(e);
}
}
console.error("Failed to decrypt message with any known session...");
this.logger.error("Failed to decrypt message with any known session...");
for (const e of errs) {
console.error("Session error:" + e, e.stack);
this.logger.error({ err: e, stack: e.stack }, "Session decryption error");
}
throw new errors.SessionError("No matching sessions found for message");
}
Expand All @@ -179,7 +181,7 @@ class SessionCipher {
// was the most current. Simply make a note of it and continue. If our
// actual open session is for reason invalid, that must be handled via
// a full SessionError response.
console.warn("Decrypted message with closed session.");
this.logger.warn("Decrypted message with closed session.");
}
await this.storeRecord(record);
return result.plaintext;
Expand All @@ -201,7 +203,7 @@ class SessionCipher {
}
record = new SessionRecord();
}
const builder = new SessionBuilder(this.storage, this.addr);
const builder = new SessionBuilder(this.storage, this.addr, this.logger);
const preKeyId = await builder.initIncoming(record, preKeyProto);
const session = record.getSession(preKeyProto.baseKey);
const plaintext = await this.doDecryptWhisperMessage(preKeyProto.message, session);
Expand Down Expand Up @@ -325,7 +327,7 @@ class SessionCipher {
if (record) {
const openSession = record.getOpenSession();
if (openSession) {
record.closeSession(openSession);
record.closeSession(openSession, this.logger);
await this.storeRecord(record);
}
}
Expand Down
Loading