From 947129ac88e6c65930eb036c3eb44bff6bda8300 Mon Sep 17 00:00:00 2001 From: Lucas Reis Date: Sun, 21 Sep 2025 11:59:22 +0200 Subject: [PATCH 1/3] feat(core): implement standard logging --- package.json | 55 ++++++----- src/curve.js | 74 ++++++++------- src/queue_job.js | 33 ++++--- src/session_builder.js | 84 +++++++++++------ src/session_cipher.js | 97 +++++++++++--------- src/session_record.js | 95 ++++++++++--------- src/utils/logger.js | 201 +++++++++++++++++++++++++++++++++++++++++ yarn.lock | 166 +++++++++++++++++++++++++++++++++- 8 files changed, 623 insertions(+), 182 deletions(-) create mode 100644 src/utils/logger.js diff --git a/package.json b/package.json index 02814cff..80350be2 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,34 @@ { - "name": "@whiskeysockets/libsignal-node", - "version": "2.0.1", - "description": "Open Whisper Systems' libsignal for Node.js", - "repository": "WhiskeySockets/libsignal-node", - "main": "index.js", - "types": "index.d.ts", - "keywords": [ - "signal", - "whispersystems", - "crypto" - ], - "license": "GPL-3.0", - "dependencies": { - "curve25519-js": "^0.0.4", - "protobufjs": "6.8.8" - }, - "files": [ - "src/*", - "index.d.ts" - ], - "devDependencies": { - "eslint": "6.0.1" - } + "name": "@whiskeysockets/libsignal-node", + "version": "2.0.1", + "description": "Open Whisper Systems' libsignal for Node.js", + "repository": "WhiskeySockets/libsignal-node", + "main": "index.js", + "types": "index.d.ts", + "keywords": [ + "signal", + "whispersystems", + "crypto" + ], + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "pino": "^9.11.0", + "pino-pretty": "^13.1.1", + "protobufjs": "6.8.8" + }, + "files": [ + "src/*", + "index.d.ts" + ], + "devDependencies": { + "eslint": "6.0.1" + }, + "prettier": { + "printWidth": 100, + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "trailingComma": "none" + } } diff --git a/src/curve.js b/src/curve.js index 55b4b809..f6fd56df 100644 --- a/src/curve.js +++ b/src/curve.js @@ -1,26 +1,25 @@ - 'use strict'; const curveJs = require('curve25519-js'); const nodeCrypto = require('crypto'); +const { createLogger } = require('./utils/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 -]); - +const PUBLIC_KEY_DER_PREFIX = Buffer.from([48, 42, 48, 5, 6, 3, 43, 101, 110, 3, 33, 0]); + const PRIVATE_KEY_DER_PREFIX = Buffer.from([ 48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 110, 4, 34, 4, 32 ]); const KEY_BUNDLE_TYPE = Buffer.from([5]); +const baseLogger = createLogger(); const prefixKeyInPublicKey = function (pubKey) { - return Buffer.concat([KEY_BUNDLE_TYPE, pubKey]); + return Buffer.concat([KEY_BUNDLE_TYPE, pubKey]); }; function validatePrivKey(privKey) { if (privKey === undefined) { - throw new Error("Undefined private key"); + throw new Error('Undefined private key'); } if (!(privKey instanceof Buffer)) { throw new Error(`Invalid private key type: ${privKey.constructor.name}`); @@ -34,13 +33,18 @@ function scrubPubKeyFormat(pubKey) { if (!(pubKey instanceof Buffer)) { throw new Error(`Invalid public key type: ${pubKey.constructor.name}`); } - if (pubKey === undefined || ((pubKey.byteLength != 33 || pubKey[0] != 5) && pubKey.byteLength != 32)) { - throw new Error("Invalid public key"); + if ( + pubKey === undefined || + ((pubKey.byteLength != 33 || pubKey[0] != 5) && pubKey.byteLength != 32) + ) { + throw new Error('Invalid public key'); } 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"); + baseLogger.error( + 'WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey' + ); return pubKey; } } @@ -58,46 +62,50 @@ function unclampEd25519PrivateKey(clampedSk) { return unclampedSk; } -exports.getPublicFromPrivateKey = function(privKey) { +exports.getPublicFromPrivateKey = function (privKey) { const unclampedPK = unclampEd25519PrivateKey(privKey); const keyPair = curveJs.generateKeyPair(unclampedPK); return prefixKeyInPublicKey(Buffer.from(keyPair.public)); }; -exports.generateKeyPair = function() { +exports.generateKeyPair = function () { try { - const {publicKey: publicDerBytes, privateKey: privateDerBytes} = nodeCrypto.generateKeyPairSync( - 'x25519', - { + const { publicKey: publicDerBytes, privateKey: privateDerBytes } = + nodeCrypto.generateKeyPairSync('x25519', { publicKeyEncoding: { format: 'der', type: 'spki' }, privateKeyEncoding: { format: 'der', type: 'pkcs8' } - } + }); + const pubKey = publicDerBytes.slice( + PUBLIC_KEY_DER_PREFIX.length, + PUBLIC_KEY_DER_PREFIX.length + 32 ); - const pubKey = publicDerBytes.slice(PUBLIC_KEY_DER_PREFIX.length, PUBLIC_KEY_DER_PREFIX.length + 32); - - const privKey = privateDerBytes.slice(PRIVATE_KEY_DER_PREFIX.length, PRIVATE_KEY_DER_PREFIX.length + 32); - + + const privKey = privateDerBytes.slice( + PRIVATE_KEY_DER_PREFIX.length, + PRIVATE_KEY_DER_PREFIX.length + 32 + ); + return { pubKey: prefixKeyInPublicKey(pubKey), privKey }; - } catch(e) { + } catch (e) { const keyPair = curveJs.generateKeyPair(nodeCrypto.randomBytes(32)); return { privKey: Buffer.from(keyPair.private), - pubKey: prefixKeyInPublicKey(Buffer.from(keyPair.public)), + pubKey: prefixKeyInPublicKey(Buffer.from(keyPair.public)) }; } }; -exports.calculateAgreement = function(pubKey, privKey) { +exports.calculateAgreement = function (pubKey, privKey) { pubKey = scrubPubKeyFormat(pubKey); validatePrivKey(privKey); if (!pubKey || pubKey.byteLength != 32) { - throw new Error("Invalid public key"); + throw new Error('Invalid public key'); } - if(typeof nodeCrypto.diffieHellman === 'function') { + if (typeof nodeCrypto.diffieHellman === 'function') { const nodePrivateKey = nodeCrypto.createPrivateKey({ key: Buffer.concat([PRIVATE_KEY_DER_PREFIX, privKey]), format: 'der', @@ -108,10 +116,10 @@ exports.calculateAgreement = function(pubKey, privKey) { format: 'der', type: 'spki' }); - + return nodeCrypto.diffieHellman({ privateKey: nodePrivateKey, - publicKey: nodePublicKey, + publicKey: nodePublicKey }); } else { const secret = curveJs.sharedKey(privKey, pubKey); @@ -119,24 +127,24 @@ exports.calculateAgreement = function(pubKey, privKey) { } }; -exports.calculateSignature = function(privKey, message) { +exports.calculateSignature = function (privKey, message) { validatePrivKey(privKey); if (!message) { - throw new Error("Invalid message"); + throw new Error('Invalid message'); } return Buffer.from(curveJs.sign(privKey, message)); }; -exports.verifySignature = function(pubKey, msg, sig, isInit) { +exports.verifySignature = function (pubKey, msg, sig, isInit) { pubKey = scrubPubKeyFormat(pubKey); if (!pubKey || pubKey.byteLength != 32) { - throw new Error("Invalid public key"); + throw new Error('Invalid public key'); } if (!msg) { - throw new Error("Invalid message"); + throw new Error('Invalid message'); } if (!sig || sig.byteLength != 64) { - throw new Error("Invalid signature"); + throw new Error('Invalid signature'); } return isInit ? true : curveJs.verify(pubKey, msg, sig); }; diff --git a/src/queue_job.js b/src/queue_job.js index baab89c4..4bf3ca30 100644 --- a/src/queue_job.js +++ b/src/queue_job.js @@ -1,11 +1,12 @@ // vim: ts=4:sw=4:expandtab - - /* - * jobQueue manages multiple queues indexed by device to serialize - * session io ops on the database. - */ + +/* + * jobQueue manages multiple queues indexed by device to serialize + * session io ops on the database. + */ 'use strict'; +const { createLogger } = require('./utils/logger'); const _queueAsyncBuckets = new Map(); const _gcLimit = 10000; @@ -18,7 +19,7 @@ async function _asyncQueueExecutor(queue, cleanup) { const job = queue[i]; try { job.resolve(await job.awaitable()); - } catch(e) { + } catch (e) { job.reject(e); } } @@ -37,17 +38,19 @@ async function _asyncQueueExecutor(queue, cleanup) { cleanup(); } -module.exports = function(bucket, awaitable) { +module.exports = function (bucket, awaitable) { + const logger = createLogger('QueueJob'); + /* 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. */ if (!awaitable.name) { // Make debuging easier by adding a name to this function. - Object.defineProperty(awaitable, 'name', {writable: true}); + Object.defineProperty(awaitable, 'name', { writable: true }); 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; @@ -56,11 +59,13 @@ module.exports = function(bucket, awaitable) { inactive = true; } const queue = _queueAsyncBuckets.get(bucket); - const job = new Promise((resolve, reject) => queue.push({ - awaitable, - resolve, - reject - })); + const job = new Promise((resolve, reject) => + queue.push({ + awaitable, + resolve, + reject + }) + ); if (inactive) { /* An executor is not currently active; Start one now. */ _asyncQueueExecutor(queue, () => _queueAsyncBuckets.delete(bucket)); diff --git a/src/session_builder.js b/src/session_builder.js index 7b7d8520..b28f72c9 100644 --- a/src/session_builder.js +++ b/src/session_builder.js @@ -1,4 +1,3 @@ - 'use strict'; const BaseKeyType = require('./base_key_type'); @@ -8,28 +7,39 @@ const crypto = require('./crypto'); const curve = require('./curve'); const errors = require('./errors'); const queueJob = require('./queue_job'); - +const { createLogger } = require('./utils/logger'); class SessionBuilder { - constructor(storage, protocolAddress) { this.addr = protocolAddress; this.storage = storage; + + this.logger = createLogger('SessionBuilder'); } async initOutgoing(device) { const fqAddr = this.addr.toString(); return await queueJob(fqAddr, async () => { - if (!await this.storage.isTrustedIdentity(this.addr.id, device.identityKey)) { + if (!(await this.storage.isTrustedIdentity(this.addr.id, device.identityKey))) { throw new errors.UntrustedIdentityKeyError(this.addr.id, device.identityKey); } - curve.verifySignature(device.identityKey, device.signedPreKey.publicKey, - device.signedPreKey.signature, true); + curve.verifySignature( + device.identityKey, + device.signedPreKey.publicKey, + device.signedPreKey.signature, + true + ); const baseKey = curve.generateKeyPair(); const devicePreKey = device.preKey && device.preKey.publicKey; - const session = await this.initSession(true, baseKey, undefined, device.identityKey, - devicePreKey, device.signedPreKey.publicKey, - device.registrationId); + const session = await this.initSession( + true, + baseKey, + undefined, + device.identityKey, + devicePreKey, + device.signedPreKey.publicKey, + device.registrationId + ); session.pendingPreKey = { signedKeyId: device.signedPreKey.keyId, baseKey: baseKey.pubKey @@ -43,7 +53,7 @@ class SessionBuilder { } else { const openSession = record.getOpenSession(); if (openSession) { - console.warn("Closing stale open session for new outgoing prekey bundle"); + this.logger.warn('Closing stale open session for new outgoing prekey bundle'); record.closeSession(openSession); } } @@ -54,7 +64,7 @@ class SessionBuilder { async initIncoming(record, message) { const fqAddr = this.addr.toString(); - if (!await this.storage.isTrustedIdentity(fqAddr, message.identityKey)) { + if (!(await this.storage.isTrustedIdentity(fqAddr, message.identityKey))) { throw new errors.UntrustedIdentityKeyError(this.addr.id, message.identityKey); } if (record.getSession(message.baseKey)) { @@ -64,32 +74,47 @@ class SessionBuilder { const preKeyPair = await this.storage.loadPreKey(message.preKeyId); if (message.preKeyId && !preKeyPair) { throw new errors.PreKeyError('Invalid PreKey ID'); - } + } const signedPreKeyPair = await this.storage.loadSignedPreKey(message.signedPreKeyId); - if (!signedPreKeyPair) { - throw new errors.PreKeyError("Missing SignedPreKey"); - } + if (!signedPreKeyPair) { + throw new errors.PreKeyError('Missing SignedPreKey'); + } const existingOpenSession = record.getOpenSession(); if (existingOpenSession) { - console.warn("Closing open session in favor of incoming prekey bundle"); + this.logger.warn('Closing open session in favor of incoming prekey bundle'); record.closeSession(existingOpenSession); } - record.setSession(await this.initSession(false, preKeyPair, signedPreKeyPair, - message.identityKey, message.baseKey, - undefined, message.registrationId)); + record.setSession( + await this.initSession( + false, + preKeyPair, + signedPreKeyPair, + message.identityKey, + message.baseKey, + undefined, + message.registrationId + ) + ); return message.preKeyId; } - async initSession(isInitiator, ourEphemeralKey, ourSignedKey, theirIdentityPubKey, - theirEphemeralPubKey, theirSignedPubKey, registrationId) { + async initSession( + isInitiator, + ourEphemeralKey, + ourSignedKey, + theirIdentityPubKey, + theirEphemeralPubKey, + theirSignedPubKey, + registrationId + ) { if (isInitiator) { if (ourSignedKey) { - throw new Error("Invalid call to initSession"); + throw new Error('Invalid call to initSession'); } ourSignedKey = ourEphemeralKey; } else { if (theirSignedPubKey) { - throw new Error("Invalid call to initSession"); + throw new Error('Invalid call to initSession'); } theirSignedPubKey = theirEphemeralPubKey; } @@ -118,8 +143,11 @@ class SessionBuilder { const a4 = curve.calculateAgreement(theirEphemeralPubKey, ourEphemeralKey.privKey); sharedSecret.set(new Uint8Array(a4), 32 * 4); } - const masterKey = crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32), - Buffer.from("WhisperText")); + const masterKey = crypto.deriveSecrets( + Buffer.from(sharedSecret), + Buffer.alloc(32), + Buffer.from('WhisperText') + ); const session = SessionRecord.createEntry(); session.registrationId = registrationId; session.currentRatchet = { @@ -148,7 +176,11 @@ class SessionBuilder { calculateSendingRatchet(session, remoteKey) { const ratchet = session.currentRatchet; const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey); - const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet")); + const masterKey = crypto.deriveSecrets( + sharedSecret, + ratchet.rootKey, + Buffer.from('WhisperRatchet') + ); session.addChain(ratchet.ephemeralKeyPair.pubKey, { messageKeys: {}, chainKey: { diff --git a/src/session_cipher.js b/src/session_cipher.js index 0e6df11e..20c05cef 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 { createLogger } = require('./utils/logger'); const VERSION = 3; @@ -19,20 +20,20 @@ function assertBuffer(value) { return value; } - class SessionCipher { - constructor(storage, protocolAddress) { if (!(protocolAddress instanceof ProtocolAddress)) { - throw new TypeError("protocolAddress must be a ProtocolAddress"); + throw new TypeError('protocolAddress must be a ProtocolAddress'); } this.addr = protocolAddress; this.storage = storage; + + this.logger = createLogger('SessionCipher'); } _encodeTupleByte(number1, number2) { if (number1 > 15 || number2 > 15) { - throw TypeError("Numbers must be 4 bits or less"); + throw TypeError('Numbers must be 4 bits or less'); } return (number1 << 4) | number2; } @@ -48,7 +49,7 @@ class SessionCipher { async getRecord() { const record = await this.storage.loadSession(this.addr.toString()); if (record && !(record instanceof SessionRecord)) { - throw new TypeError('SessionRecord type expected from loadSession'); + throw new TypeError('SessionRecord type expected from loadSession'); } return record; } @@ -68,23 +69,26 @@ class SessionCipher { return await this.queueJob(async () => { const record = await this.getRecord(); if (!record) { - throw new errors.SessionError("No sessions"); + throw new errors.SessionError('No sessions'); } const session = record.getOpenSession(); if (!session) { - throw new errors.SessionError("No open session"); + throw new errors.SessionError('No open session'); } const remoteIdentityKey = session.indexInfo.remoteIdentityKey; - if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) { + if (!(await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey))) { throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey); } const chain = session.getChain(session.currentRatchet.ephemeralKeyPair.pubKey); if (chain.chainType === ChainType.RECEIVING) { - throw new Error("Tried to encrypt on a receiving chain"); + throw new Error('Tried to encrypt on a receiving chain'); } this.fillMessageKeys(chain, chain.chainKey.counter + 1); - const keys = crypto.deriveSecrets(chain.messageKeys[chain.chainKey.counter], - Buffer.alloc(32), Buffer.from("WhisperMessageKeys")); + const keys = crypto.deriveSecrets( + chain.messageKeys[chain.chainKey.counter], + Buffer.alloc(32), + Buffer.from('WhisperMessageKeys') + ); delete chain.messageKeys[chain.chainKey.counter]; const msg = protobufs.WhisperMessage.create(); msg.ephemeralKey = session.currentRatchet.ephemeralKeyPair.pubKey; @@ -92,11 +96,11 @@ class SessionCipher { msg.previousCounter = session.currentRatchet.previousCounter; msg.ciphertext = crypto.encrypt(keys[0], data, keys[2].slice(0, 16)); const msgBuf = protobufs.WhisperMessage.encode(msg).finish(); - const macInput = Buffer.alloc(msgBuf.byteLength + (33 * 2) + 1); + const macInput = Buffer.alloc(msgBuf.byteLength + 33 * 2 + 1); macInput.set(ourIdentityKey.pubKey); macInput.set(session.indexInfo.remoteIdentityKey, 33); macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION); - macInput.set(msgBuf, (33 * 2) + 1); + macInput.set(msgBuf, 33 * 2 + 1); const mac = crypto.calculateMAC(keys[1], macInput); const result = Buffer.alloc(msgBuf.byteLength + 9); result[0] = this._encodeTupleByte(VERSION, VERSION); @@ -105,7 +109,7 @@ class SessionCipher { await this.storeRecord(record); let type, body; if (session.pendingPreKey) { - type = 3; // prekey bundle + type = 3; // prekey bundle const preKeyMsg = protobufs.PreKeyWhisperMessage.create({ identityKey: ourIdentityKey.pubKey, registrationId: await this.storage.getOurRegistrationId(), @@ -118,12 +122,10 @@ class SessionCipher { } body = Buffer.concat([ Buffer.from([this._encodeTupleByte(VERSION, VERSION)]), - Buffer.from( - protobufs.PreKeyWhisperMessage.encode(preKeyMsg).finish() - ) + Buffer.from(protobufs.PreKeyWhisperMessage.encode(preKeyMsg).finish()) ]); } else { - type = 1; // normal + type = 1; // normal body = result; } return { @@ -138,11 +140,11 @@ class SessionCipher { // Iterate through the sessions, attempting to decrypt using each one. // Stop and return the result if we get a valid result. if (!sessions.length) { - throw new errors.SessionError("No sessions available"); - } + throw new errors.SessionError('No sessions available'); + } const errs = []; for (const session of sessions) { - let plaintext; + let plaintext; try { plaintext = await this.doDecryptWhisperMessage(data, session); session.indexInfo.used = Date.now(); @@ -150,15 +152,15 @@ class SessionCipher { session, plaintext }; - } catch(e) { + } catch (e) { 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('Session error:' + e, e.stack); } - throw new errors.SessionError("No matching sessions found for message"); + throw new errors.SessionError('No matching sessions found for message'); } async decryptWhisperMessage(data) { @@ -166,20 +168,20 @@ class SessionCipher { return await this.queueJob(async () => { const record = await this.getRecord(); if (!record) { - throw new errors.SessionError("No session record"); + throw new errors.SessionError('No session record'); } const result = await this.decryptWithSessions(data, record.getSessions()); const remoteIdentityKey = result.session.indexInfo.remoteIdentityKey; - if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) { + if (!(await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey))) { throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey); - } + } if (record.isClosed(result.session)) { // It's possible for this to happen when processing a backlog of messages. // The message was, hopefully, just sent back in a time when this session // 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; @@ -189,15 +191,16 @@ class SessionCipher { async decryptPreKeyWhisperMessage(data) { assertBuffer(data); const versions = this._decodeTupleByte(data[0]); - if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3 - throw new Error("Incompatible version number on PreKeyWhisperMessage"); + if (versions[1] > 3 || versions[0] < 3) { + // min version > 3 or max version < 3 + throw new Error('Incompatible version number on PreKeyWhisperMessage'); } return await this.queueJob(async () => { let record = await this.getRecord(); const preKeyProto = protobufs.PreKeyWhisperMessage.decode(data.slice(1)); if (!record) { if (preKeyProto.registrationId == null) { - throw new Error("No registrationId"); + throw new Error('No registrationId'); } record = new SessionRecord(); } @@ -216,18 +219,19 @@ class SessionCipher { async doDecryptWhisperMessage(messageBuffer, session) { assertBuffer(messageBuffer); if (!session) { - throw new TypeError("session required"); + throw new TypeError('session required'); } const versions = this._decodeTupleByte(messageBuffer[0]); - if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3 - throw new Error("Incompatible version number on WhisperMessage"); + if (versions[1] > 3 || versions[0] < 3) { + // min version > 3 or max version < 3 + throw new Error('Incompatible version number on WhisperMessage'); } const messageProto = messageBuffer.slice(1, -8); const message = protobufs.WhisperMessage.decode(messageProto); this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter); const chain = session.getChain(message.ephemeralKey); if (chain.chainType === ChainType.SENDING) { - throw new Error("Tried to decrypt on a sending chain"); + throw new Error('Tried to decrypt on a sending chain'); } this.fillMessageKeys(chain, message.counter); if (!chain.messageKeys.hasOwnProperty(message.counter)) { @@ -237,14 +241,17 @@ class SessionCipher { } const messageKey = chain.messageKeys[message.counter]; delete chain.messageKeys[message.counter]; - const keys = crypto.deriveSecrets(messageKey, Buffer.alloc(32), - Buffer.from("WhisperMessageKeys")); + const keys = crypto.deriveSecrets( + messageKey, + Buffer.alloc(32), + Buffer.from('WhisperMessageKeys') + ); const ourIdentityKey = await this.storage.getOurIdentity(); - const macInput = Buffer.alloc(messageProto.byteLength + (33 * 2) + 1); + const macInput = Buffer.alloc(messageProto.byteLength + 33 * 2 + 1); macInput.set(session.indexInfo.remoteIdentityKey); macInput.set(ourIdentityKey.pubKey, 33); macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION); - macInput.set(messageProto, (33 * 2) + 1); + macInput.set(messageProto, 33 * 2 + 1); // This is where we most likely fail if the session is not a match. // Don't misinterpret this as corruption. crypto.verifyMAC(macInput, keys[1], messageBuffer.slice(-8), 8); @@ -278,7 +285,7 @@ class SessionCipher { let previousRatchet = session.getChain(ratchet.lastRemoteEphemeralKey); if (previousRatchet) { this.fillMessageKeys(previousRatchet, previousCounter); - delete previousRatchet.chainKey.key; // Close + delete previousRatchet.chainKey.key; // Close } this.calculateRatchet(session, remoteKey, false); // Now swap the ephemeral key and calculate the new sending chain @@ -295,8 +302,12 @@ class SessionCipher { calculateRatchet(session, remoteKey, sending) { let ratchet = session.currentRatchet; const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey); - const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, - Buffer.from("WhisperRatchet"), /*chunks*/ 2); + const masterKey = crypto.deriveSecrets( + sharedSecret, + ratchet.rootKey, + Buffer.from('WhisperRatchet'), + /*chunks*/ 2 + ); const chainKey = sending ? ratchet.ephemeralKeyPair.pubKey : remoteKey; session.addChain(chainKey, { messageKeys: {}, diff --git a/src/session_record.js b/src/session_record.js index 7626a392..b19f2a71 100644 --- a/src/session_record.js +++ b/src/session_record.js @@ -1,26 +1,29 @@ // vim: ts=4:sw=4 const BaseKeyType = require('./base_key_type'); +const { createLogger } = require('./utils/logger'); const CLOSED_SESSIONS_MAX = 40; const SESSION_RECORD_VERSION = 'v1'; +const baseLogger = createLogger(); + function assertBuffer(value) { if (!Buffer.isBuffer(value)) { - throw new TypeError("Buffer required"); + throw new TypeError('Buffer required'); } } - class SessionEntry { - constructor() { this._chains = {}; + + this.logger = createLogger('SessionEntry'); } toString() { - const baseKey = this.indexInfo && this.indexInfo.baseKey && - this.indexInfo.baseKey.toString('base64'); + const baseKey = + this.indexInfo && this.indexInfo.baseKey && this.indexInfo.baseKey.toString('base64'); return ``; } @@ -32,7 +35,7 @@ class SessionEntry { assertBuffer(key); const id = key.toString('base64'); if (this._chains.hasOwnProperty(id)) { - throw new Error("Overwrite attempt"); + throw new Error('Overwrite attempt'); } this._chains[id] = value; } @@ -46,7 +49,7 @@ class SessionEntry { assertBuffer(key); const id = key.toString('base64'); if (!this._chains.hasOwnProperty(id)) { - throw new ReferenceError("Not Found"); + throw new ReferenceError('Not Found'); } delete this._chains[id]; } @@ -65,7 +68,8 @@ class SessionEntry { pubKey: this.currentRatchet.ephemeralKeyPair.pubKey.toString('base64'), privKey: this.currentRatchet.ephemeralKeyPair.privKey.toString('base64') }, - lastRemoteEphemeralKey: this.currentRatchet.lastRemoteEphemeralKey.toString('base64'), + lastRemoteEphemeralKey: + this.currentRatchet.lastRemoteEphemeralKey.toString('base64'), previousCounter: this.currentRatchet.previousCounter, rootKey: this.currentRatchet.rootKey.toString('base64') }, @@ -94,7 +98,10 @@ class SessionEntry { pubKey: Buffer.from(data.currentRatchet.ephemeralKeyPair.pubKey, 'base64'), privKey: Buffer.from(data.currentRatchet.ephemeralKeyPair.privKey, 'base64') }, - lastRemoteEphemeralKey: Buffer.from(data.currentRatchet.lastRemoteEphemeralKey, 'base64'), + lastRemoteEphemeralKey: Buffer.from( + data.currentRatchet.lastRemoteEphemeralKey, + 'base64' + ), previousCounter: data.currentRatchet.previousCounter, rootKey: Buffer.from(data.currentRatchet.rootKey, 'base64') }; @@ -153,51 +160,53 @@ class SessionEntry { } return r; } - } - -const migrations = [{ - version: 'v1', - migrate: function migrateV1(data) { - const sessions = data._sessions; - if (data.registrationId) { - for (const key in sessions) { - if (!sessions[key].registrationId) { - sessions[key].registrationId = data.registrationId; +const migrations = [ + { + version: 'v1', + migrate: function migrateV1(data) { + const sessions = data._sessions; + if (data.registrationId) { + for (const key in sessions) { + if (!sessions[key].registrationId) { + sessions[key].registrationId = data.registrationId; + } } - } - } 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); + } else { + for (const key in sessions) { + if (sessions[key].indexInfo.closed === -1) { + baseLogger.error( + 'V1 session storage migration error: registrationId is missing', + { + registrationId: data.registrationId, + version: data.version + } + ); + } } } } } -}]; - +]; class SessionRecord { - static createEntry() { return new SessionEntry(); } static migrate(data) { - let run = (data.version === undefined); + let run = data.version === undefined; for (let i = 0; i < migrations.length; ++i) { if (run) { - console.info("Migrating session to:", migrations[i].version); + this.logger.info('Migrating session to:', migrations[i].version); migrations[i].migrate(data); } else if (migrations[i].version === data.version) { run = true; } } if (!run) { - throw new Error("Error migrating SessionRecord"); + throw new Error('Error migrating SessionRecord'); } } @@ -217,6 +226,8 @@ class SessionRecord { constructor() { this.sessions = {}; this.version = SESSION_RECORD_VERSION; + + this.logger = createLogger('SessionRecord'); } serialize() { @@ -232,14 +243,14 @@ class SessionRecord { haveOpenSession() { const openSession = this.getOpenSession(); - return (!!openSession && typeof openSession.registrationId === 'number'); + return !!openSession && typeof openSession.registrationId === 'number'; } getSession(key) { assertBuffer(key); const session = this.sessions[key.toString('base64')]; if (session && session.indexInfo.baseKeyType === BaseKeyType.OURS) { - throw new Error("Tried to lookup a session using our basekey"); + throw new Error('Tried to lookup a session using our basekey'); } return session; } @@ -267,18 +278,18 @@ class SessionRecord { closeSession(session) { if (this.isClosed(session)) { - console.warn("Session already closed", session); + this.logger.warn('Session already closed', session); return; } - console.info("Closing session:", session); + this.logger.info('Closing session:', session); session.indexInfo.closed = Date.now(); } openSession(session) { if (!this.isClosed(session)) { - console.warn("Session already open"); + this.logger.warn('Session already open'); } - console.info("Opening session:", session); + this.logger.info('Opening session:', session); session.indexInfo.closed = -1; } @@ -291,14 +302,16 @@ class SessionRecord { let oldestKey; let oldestSession; for (const [key, session] of Object.entries(this.sessions)) { - if (session.indexInfo.closed !== -1 && - (!oldestSession || session.indexInfo.closed < oldestSession.indexInfo.closed)) { + if ( + session.indexInfo.closed !== -1 && + (!oldestSession || session.indexInfo.closed < oldestSession.indexInfo.closed) + ) { oldestKey = key; oldestSession = session; } } if (oldestKey) { - console.info("Removing old closed session:", oldestSession); + this.logger.info('Removing old closed session:', oldestSession); delete this.sessions[oldestKey]; } else { throw new Error('Corrupt sessions object'); diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 00000000..6a1ad168 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,201 @@ +const { pino } = require('pino'); + +const getDefaultLogLevel = () => { + const isDebug = process.env.DEBUG || process.env.NODE_ENV === 'development'; + + if (isDebug) { + return 'debug'; + } + + return process.env.LOG_LEVEL || 'info'; +}; + +const baseConfig = { + level: getDefaultLogLevel() +}; + +const getLoggerConfig = + process.env.NODE_ENV !== 'production' + ? { + transport: { + target: 'pino-pretty', + level: 'debug', + options: { + singleLine: true, + colorize: true, + colorizeObjects: true, + ignore: 'hostname,pid', + messageKey: 'message' + } + } + } + : {}; + +const baseLogger = pino( + Object.assign({}, baseConfig, getLoggerConfig, { + messageKey: 'message' + }) +); + +/** + * LoggerService is a class that provides a logger for a given service. + * @param {string} serviceName - The name of the service to log. + * @param {object} options - The options for the logger. + * @param {string[]} options.ignoredContexts - The contexts to ignore. + * @param {string[]} options.allowedErrorLogLevels - The levels to log as errors. + * @param {string[]} options.unignoredLevels - The levels to log as errors. + * + * @returns {LoggerService} - Logger instance + */ +class LoggerService { + constructor(serviceName = 'libsignal-node', options = {}) { + this.logger = baseLogger.child({ + serviceName + }); + + this.MAX_ERROR_DEPTH = 3; + this.ignoredContexts = options.ignoredContexts || []; + this.allowedErrorLogLevels = options.allowedErrorLogLevels || ['error', 'warn']; + this.unignoredLevels = options.unignoredLevels || ['error', 'warn']; + } + + /** + * Since pino doesn't log errors properly, we format the error object + * to a more readable format. + * @param {*} error + * @returns + */ + formatError(error) { + const { message, name, stack } = error; + const rest = {}; + + for (const key in error) { + if (key !== 'message' && key !== 'name' && key !== 'stack') { + rest[key] = error[key]; + } + } + + return { + error: Object.assign( + { + name, + message, + stack + }, + rest + ) + }; + } + + /** + * Processes an object and its nested objects to log errors properly. + * @param {object} obj - The object to process. + * @param {number} depth - The depth of the object. + * @returns {object} - The processed object. + */ + processErrorsInObject(obj, depth = 0) { + if (obj === null || obj === undefined) { + return obj; + } + + if (depth >= this.MAX_ERROR_DEPTH) { + return obj; + } + + if (obj instanceof Error) { + return this.formatError(obj); + } + + if (Array.isArray(obj)) { + return obj.map((item) => this.processErrorsInObject(item, depth + 1)); + } + + if (obj !== null && typeof obj === 'object' && obj.constructor === Object) { + return Object.entries(obj).reduce((result, [key, value]) => { + result[key] = this.processErrorsInObject(value, depth + 1); + return result; + }, {}); + } + + return obj; + } + + /** + * Logs a message with a given level. + * @param {string} level - The level of the message. + * @param {string} msg - The message to log. + * @param {object} obj - The object to log. + * @param {...*} args - The arguments to log. + */ + logWithLevel(level, msg, obj, ...args) { + const processedObj = + this.allowedErrorLogLevels.includes(level) && + obj !== null && + typeof obj === 'object' && + obj.constructor === Object + ? this.processErrorsInObject(obj) + : obj != null + ? obj + : undefined; + + const processedArgs = this.allowedErrorLogLevels.includes(level) + ? args.map((arg) => this.processErrorsInObject(arg)) + : args; + + if (processedObj === undefined) { + this.logger[level](msg, ...processedArgs); + } else if (typeof obj === 'string') { + if (this.ignoredContexts.includes(obj) && !this.unignoredLevels.includes(level)) { + return; + } + + this.logger[level]({ $context: obj }, msg, ...processedArgs); + } else { + this.logger[level](processedObj, msg, ...processedArgs); + } + } + + /** + * Logs an info message. + * @param {string} message - The message to log. + * @param {object} data - The data to log. + */ + info(message, data) { + this.logWithLevel('info', message, data); + } + + /** + * Logs an error message. + * @param {string} message - The message to log. + * @param {object} data - The data to log. + */ + error(message, data) { + this.logWithLevel('error', message, data); + } + + /** + * Logs a warning message. + * @param {string} message - The message to log. + * @param {object} data - The data to log. + */ + warn(message, data) { + this.logWithLevel('warn', message, data); + } + + /** + * Logs a debug message. + * @param {string} message - The message to log. + * @param {object} data - The data to log. + */ + debug(message, data) { + this.logWithLevel('debug', message, data); + } +} + +const createLogger = (loggerName = 'libsignal-node') => { + return new LoggerService(loggerName); +}; + +module.exports = { + createLogger +}; diff --git a/yarn.lock b/yarn.lock index 2cfe7890..01a3ce70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -196,6 +201,11 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +colorette@^2.0.7: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -217,6 +227,11 @@ curve25519-js@^0.0.4: resolved "https://registry.yarnpkg.com/curve25519-js/-/curve25519-js-0.0.4.tgz#e6ad967e8cd284590d657bbfc90d8b50e49ba060" integrity sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w== +dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + debug@^4.0.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" @@ -241,6 +256,13 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +end-of-stream@^1.1.0: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -360,6 +382,11 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" +fast-copy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35" + integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== + fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -375,6 +402,16 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -443,6 +480,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +help-me@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" + integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== + iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -529,6 +571,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -587,6 +634,11 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -614,10 +666,15 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -once@^1.3.0: +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -667,11 +724,64 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +pino-abstract-transport@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" + integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== + dependencies: + split2 "^4.0.0" + +pino-pretty@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-13.1.1.tgz#70130b9ff3737c8757f53e42d69e9f96835142b8" + integrity sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^3.0.2" + fast-safe-stringify "^2.1.1" + help-me "^5.0.0" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pump "^3.0.0" + secure-json-parse "^4.0.0" + sonic-boom "^4.0.1" + strip-json-comments "^5.0.2" + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^9.11.0: + version "9.11.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.11.0.tgz#7fc383f815cf6bf5979b4d791eafd2f2496c42a6" + integrity sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -696,11 +806,29 @@ protobufjs@6.8.8: "@types/node" "^10.1.0" long "^4.0.0" +pump@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" + integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -738,11 +866,21 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +secure-json-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-4.0.0.tgz#2ee1b7581be38ab348bab5a3e49280ba80a89c85" + integrity sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA== + semver@^5.5.0, semver@^5.5.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -774,6 +912,18 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +sonic-boom@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d" + integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww== + dependencies: + atomic-sleep "^1.0.0" + +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -815,6 +965,11 @@ strip-json-comments@^2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-json-comments@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-5.0.3.tgz#b7304249dd402ee67fd518ada993ab3593458bcf" + integrity sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw== + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -837,6 +992,13 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" From 52515a654ccb7040c9a76d3608bc2356495fb559 Mon Sep 17 00:00:00 2001 From: Lucas Reis Date: Sun, 21 Sep 2025 12:01:14 +0200 Subject: [PATCH 2/3] fix: identation in package.json --- package.json | 64 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 80350be2..c57b1f70 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,34 @@ { - "name": "@whiskeysockets/libsignal-node", - "version": "2.0.1", - "description": "Open Whisper Systems' libsignal for Node.js", - "repository": "WhiskeySockets/libsignal-node", - "main": "index.js", - "types": "index.d.ts", - "keywords": [ - "signal", - "whispersystems", - "crypto" - ], - "license": "GPL-3.0", - "dependencies": { - "curve25519-js": "^0.0.4", - "pino": "^9.11.0", - "pino-pretty": "^13.1.1", - "protobufjs": "6.8.8" - }, - "files": [ - "src/*", - "index.d.ts" - ], - "devDependencies": { - "eslint": "6.0.1" - }, - "prettier": { - "printWidth": 100, - "tabWidth": 4, - "useTabs": false, - "singleQuote": true, - "trailingComma": "none" - } + "name": "@whiskeysockets/libsignal-node", + "version": "2.0.1", + "description": "Open Whisper Systems' libsignal for Node.js", + "repository": "WhiskeySockets/libsignal-node", + "main": "index.js", + "types": "index.d.ts", + "keywords": [ + "signal", + "whispersystems", + "crypto" + ], + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "pino": "^9.11.0", + "pino-pretty": "^13.1.1", + "protobufjs": "6.8.8" + }, + "files": [ + "src/*", + "index.d.ts" + ], + "devDependencies": { + "eslint": "6.0.1" + }, + "prettier": { + "printWidth": 100, + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "trailingComma": "none" + } } From 625fa4c300c2d49f06c656571b1309d91dea5862 Mon Sep 17 00:00:00 2001 From: Lucas Reis Date: Sun, 21 Sep 2025 12:05:57 +0200 Subject: [PATCH 3/3] feat(logger): enhance logging functionality with environment-based log level configuration --- src/utils/logger.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/utils/logger.js b/src/utils/logger.js index 6a1ad168..4b6a0e03 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,5 +1,12 @@ const { pino } = require('pino'); +/** + * This function returns the log level for the loggers based on + * environment variables. To silence the libsignal logs, just set an env var + * LIBSIGNAL_LOG_LEVEL=silent + * + * @returns {string} - The default log level. + */ const getDefaultLogLevel = () => { const isDebug = process.env.DEBUG || process.env.NODE_ENV === 'development'; @@ -7,6 +14,12 @@ const getDefaultLogLevel = () => { return 'debug'; } + const hasLibsignalLogLevel = process.env.LIBSIGNAL_LOG_LEVEL; + + if (hasLibsignalLogLevel) { + return process.env.LIBSIGNAL_LOG_LEVEL; + } + return process.env.LOG_LEVEL || 'info'; };