diff --git a/index.d.ts b/index.d.ts index 94e46d8d..3adcf0ed 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; storeSession(id: string, session: SessionRecord): Promise; @@ -30,6 +38,9 @@ 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; @@ -37,19 +48,27 @@ export class ProtocolAddress { } 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; public decryptWhisperMessage(ciphertext: Uint8Array): Promise; 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; } diff --git a/src/curve.d.ts b/src/curve.d.ts index ba5437fa..92d4bf96 100644 --- a/src/curve.d.ts +++ b/src/curve.d.ts @@ -1,3 +1,5 @@ +import type { ILogger } from '../index'; + export interface KeyPairType { pubKey: Uint8Array; privKey: Uint8Array; @@ -7,7 +9,8 @@ export function generateKeyPair(): KeyPairType; export function calculateAgreement( publicKey: Uint8Array, - privateKey: Uint8Array + privateKey: Uint8Array, + logger?: ILogger ): Uint8Array; export function calculateSignature( @@ -18,5 +21,6 @@ export function calculateSignature( export function verifySignature( publicKey: Uint8Array, message: Uint8Array, - signature: Uint8Array + signature: Uint8Array, + logger?: ILogger ): boolean; diff --git a/src/curve.js b/src/curve.js index 55b4b809..1cbc904d 100644 --- a/src/curve.js +++ b/src/curve.js @@ -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 @@ -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}`); } @@ -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; } } @@ -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"); @@ -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"); } diff --git a/src/noop_logger.js b/src/noop_logger.js new file mode 100644 index 00000000..167e1d46 --- /dev/null +++ b/src/noop_logger.js @@ -0,0 +1,11 @@ +// vim: ts=4:sw=4:expandtab + +const noopLogger = { + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + trace: () => {}, +}; + +module.exports = noopLogger; diff --git a/src/queue_job.js b/src/queue_job.js index baab89c4..955346f1 100644 --- a/src/queue_job.js +++ b/src/queue_job.js @@ -6,6 +6,7 @@ */ 'use strict'; +const noopLogger = require('./noop_logger'); const _queueAsyncBuckets = new Map(); const _gcLimit = 10000; @@ -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. */ @@ -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; diff --git a/src/session_builder.js b/src/session_builder.js index 7b7d8520..964aa908 100644 --- a/src/session_builder.js +++ b/src/session_builder.js @@ -1,4 +1,3 @@ - 'use strict'; const BaseKeyType = require('./base_key_type'); @@ -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) { @@ -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) { @@ -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, diff --git a/src/session_cipher.js b/src/session_cipher.js index 0e6df11e..f933681a 100644 --- a/src/session_cipher.js +++ b/src/session_cipher.js @@ -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; @@ -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) { @@ -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) { @@ -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"); } @@ -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; @@ -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); @@ -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); } } diff --git a/src/session_record.js b/src/session_record.js index 7626a392..8ea11d9d 100644 --- a/src/session_record.js +++ b/src/session_record.js @@ -1,6 +1,7 @@ // vim: ts=4:sw=4 const BaseKeyType = require('./base_key_type'); +const noopLogger = require('./noop_logger'); const CLOSED_SESSIONS_MAX = 40; const SESSION_RECORD_VERSION = 'v1'; @@ -159,7 +160,7 @@ class SessionEntry { const migrations = [{ version: 'v1', - migrate: function migrateV1(data) { + migrate: function migrateV1(data, logger) { const sessions = data._sessions; if (data.registrationId) { for (const key in sessions) { @@ -170,9 +171,10 @@ const migrations = [{ } else { for (const key in sessions) { if (sessions[key].indexInfo.closed === -1) { - console.error('V1 session storage migration error: registrationId', - data.registrationId, 'for open session version', - data.version); + logger.error({ + registrationId: data.registrationId, + version: data.version + }, 'V1 session storage migration error'); } } } @@ -186,12 +188,13 @@ class SessionRecord { return new SessionEntry(); } - static migrate(data) { + static migrate(data, logger) { + logger = logger || noopLogger; let run = (data.version === undefined); for (let i = 0; i < migrations.length; ++i) { if (run) { - console.info("Migrating session to:", migrations[i].version); - migrations[i].migrate(data); + logger.info({ toVersion: migrations[i].version }, "Migrating session"); + migrations[i].migrate(data, logger); } else if (migrations[i].version === data.version) { run = true; } @@ -201,9 +204,10 @@ class SessionRecord { } } - static deserialize(data) { + static deserialize(data, logger) { + logger = logger || noopLogger; if (data.version !== SESSION_RECORD_VERSION) { - this.migrate(data); + this.migrate(data, logger); } const obj = new this(); if (data._sessions) { @@ -265,20 +269,22 @@ class SessionRecord { }); } - closeSession(session) { + closeSession(session, logger) { + logger = logger || noopLogger; if (this.isClosed(session)) { - console.warn("Session already closed", session); + logger.warn({ session }, "Session already closed"); return; } - console.info("Closing session:", session); + logger.debug({ session }, "Closing session"); session.indexInfo.closed = Date.now(); } - openSession(session) { + openSession(session, logger) { + logger = logger || noopLogger; if (!this.isClosed(session)) { - console.warn("Session already open"); + logger.warn({ session }, "Session already open"); } - console.info("Opening session:", session); + logger.info({ session }, "Opening session"); session.indexInfo.closed = -1; } @@ -286,7 +292,8 @@ class SessionRecord { return session.indexInfo.closed !== -1; } - removeOldSessions() { + removeOldSessions(logger) { + logger = logger || noopLogger; while (Object.keys(this.sessions).length > CLOSED_SESSIONS_MAX) { let oldestKey; let oldestSession; @@ -298,7 +305,7 @@ class SessionRecord { } } if (oldestKey) { - console.info("Removing old closed session:", oldestSession); + logger.info({ session: oldestSession }, "Removing old closed session"); delete this.sessions[oldestKey]; } else { throw new Error('Corrupt sessions object');