Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions connection/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ import {
import { performHandshake, validateTunnelRequirements, killV2RayProc, verifyDependencies } from './tunnel.js';
import { verifyConnection } from './state.js';
import { registerCleanupHandlers } from './disconnect.js';
import { withMnemonicRedaction } from './logger.js';

let defaultLog = console.log;
// Default logger wraps console.log with a mnemonic redactor. Anything that
// resembles a 12–24 word BIP-39 phrase in a log argument is replaced with
// `[REDACTED MNEMONIC]` before it reaches stdout. Defense-in-depth — the SDK
// does not currently log the mnemonic, but a future template-string bug or
// careless `JSON.stringify(opts)` won't leak it through the default logger.
let defaultLog = withMnemonicRedaction(console.log);

// ─── Shared Validation ───────────────────────────────────────────────────────

Expand Down Expand Up @@ -548,7 +554,7 @@ export async function connectAuto(opts) {
if (opts.circuitBreaker) configureCircuitBreaker(opts.circuitBreaker);

const maxAttempts = opts.maxAttempts || 3;
const logFn = opts.log || console.log;
const logFn = opts.log || defaultLog;
const errors = [];

// If nodeAddress specified, try it first (skip circuit breaker check for explicit choice)
Expand Down
7 changes: 6 additions & 1 deletion connection/disconnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import { DEFAULT_LCD, DEFAULT_TIMEOUTS } from '../defaults.js';
import { nodeStatusV3 } from '../v3protocol.js';
import { queryNode, privKeyFromMnemonic } from '../cosmjs-setup.js';
import { createNodeHttpsAgent } from '../tls-trust.js';
import { withMnemonicRedaction } from './logger.js';

// Default logger wraps console.log so any 12–24 word BIP-39 phrase that ends
// up in a log argument is replaced before reaching stdout. See connection/logger.js.
const defaultLog = withMnemonicRedaction(console.log);

// ─── Disconnect ──────────────────────────────────────────────────────────────
//
Expand Down Expand Up @@ -204,7 +209,7 @@ export async function recoverSession(opts) {
if (!opts?.nodeAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.nodeAddress');
if (!opts?.mnemonic) throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'recoverSession requires opts.mnemonic');

const logFn = opts.log || console.log;
const logFn = opts.log || defaultLog;
const onProgress = opts.onProgress || null;
const sessionId = BigInt(opts.sessionId);
const timeouts = { ...DEFAULT_TIMEOUTS, ...opts.timeouts };
Expand Down
66 changes: 66 additions & 0 deletions connection/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Redacting logger wrapper — defense-in-depth against accidental mnemonic leaks
* in SDK log output.
*
* The SDK's default logger is `console.log`. Any future bug that interpolates
* a mnemonic into a log template string (e.g. `log(`opts: ${JSON.stringify(opts)}`)`)
* would leak the BIP-39 phrase to stdout/stderr — and from there to terminal
* scrollback, CI logs, log-aggregation tools (Datadog, Loki, Sentry breadcrumbs),
* and shell history.
*
* This module wraps any logger function with a regex-based redactor that matches
* BIP-39 word sequences and replaces them with `[REDACTED MNEMONIC]` before the
* underlying logger sees them. It is NOT a substitute for the rule "do not log
* the mnemonic" — it is a safety net so that violation does not produce a leak.
*
* Performance: the regex runs only on string arguments and short-circuits if the
* argument has fewer than ~60 characters (a 12-word mnemonic is ~80 characters).
* Negligible overhead on the SDK's hot path (a few connect-time progress logs).
*/

/**
* Match 12 / 15 / 18 / 21 / 24 lowercase BIP-39-shaped words separated by single
* spaces. We deliberately don't try to validate against the full 2048-word list
* here — the goal is to catch anything that *looks* like a mnemonic and redact
* it. False positives (e.g. a long lowercase sentence) are acceptable since the
* SDK's own log strings never contain 12+ consecutive lowercase-only ASCII words.
*
* BIP-39 words are 3–8 lowercase ASCII letters (a–z), no digits, no diacritics.
*/
const MNEMONIC_REGEX = /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/g;

const REDACTED = '[REDACTED MNEMONIC]';

/**
* Redact mnemonic-shaped substrings from a single value. Strings are scanned;
* everything else is returned unchanged. We do NOT recurse into objects — the
* SDK's loggers are called with scalar args, and walking arbitrary objects
* would risk triggering custom getters that have side effects.
*
* @param {*} value
* @returns {*}
*/
function redactValue(value) {
if (typeof value !== 'string') return value;
if (value.length < 60) return value; // shortest plausible 12-word phrase ~ 60 chars
return value.replace(MNEMONIC_REGEX, REDACTED);
}

/**
* Wrap a logger function so that mnemonic-shaped strings in its arguments are
* replaced with `[REDACTED MNEMONIC]` before they reach the wrapped logger.
* Pass-through for non-function input (returns it unchanged) so callers can
* disable logging by passing `null`.
*
* @param {Function|null|undefined} logFn - underlying logger (typically console.log)
* @returns {Function|null|undefined}
*/
export function withMnemonicRedaction(logFn) {
if (typeof logFn !== 'function') return logFn;
return function redactedLog(...args) {
return logFn(...args.map(redactValue));
};
}

// Exported for tests.
export const _internal = { MNEMONIC_REGEX, redactValue };
7 changes: 6 additions & 1 deletion connection/resilience.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ import { findV2RayExe } from './tunnel.js';
import { enableKillSwitch, isKillSwitchEnabled as _isKillSwitchEnabled } from './security.js';
import { setSystemProxy, clearSystemProxy, checkPortFree } from './proxy.js';
import { connectAuto, connectViaSubscription, connectViaPlan } from './connect.js';
import { withMnemonicRedaction } from './logger.js';

// Default logger wraps console.log so any 12–24 word BIP-39 phrase that ends
// up in a log argument is replaced before reaching stdout. See connection/logger.js.
const defaultLog = withMnemonicRedaction(console.log);

// ─── Circuit Breaker ─────────────────────────────────────────────────────────
// v22: Skip nodes that repeatedly fail. Resets after TTL expires.
Expand Down Expand Up @@ -108,7 +113,7 @@ export async function tryFastReconnect(opts, state = _defaultState) {
if (!saved) return null;

const onProgress = opts.onProgress || null;
const logFn = opts.log || console.log;
const logFn = opts.log || defaultLog;
const fullTunnel = opts.fullTunnel !== false;
const killSwitch = opts.killSwitch === true;
const systemProxy = opts.systemProxy === true;
Expand Down
14 changes: 10 additions & 4 deletions node-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
SentinelError, ValidationError, NodeError, ChainError, TunnelError, ErrorCodes,
} from './errors.js';
import { createNodeHttpsAgent, publicEndpointAgent } from './tls-trust.js';
import { withMnemonicRedaction } from './connection/logger.js';

// CA-validated agent for LCD/RPC public endpoints (valid CA certs)
const httpsAgent = publicEndpointAgent;
Expand Down Expand Up @@ -128,8 +129,13 @@ export class ConnectionState {
const _activeStates = new Set();
const _defaultState = new ConnectionState();

// Default logger — can be overridden per-call via opts.log
let defaultLog = console.log;
// Default logger — can be overridden per-call via opts.log.
// Wrapped with mnemonic redaction so any 12–24 word BIP-39 phrase that ends up
// in a log argument is replaced with `[REDACTED MNEMONIC]` before reaching
// stdout. Defense-in-depth — the SDK does not currently log the mnemonic, but
// a future template-string bug or careless `JSON.stringify(opts)` won't leak
// it through the default logger.
let defaultLog = withMnemonicRedaction(console.log);

// ─── Wallet Cache ────────────────────────────────────────────────────────────
// v21: Cache wallet derivation (BIP39 → SLIP-10 is CPU-bound, ~300ms).
Expand Down Expand Up @@ -1152,7 +1158,7 @@ export async function connectAuto(opts) {
if (opts.circuitBreaker) configureCircuitBreaker(opts.circuitBreaker);

const maxAttempts = opts.maxAttempts || 3;
const logFn = opts.log || console.log;
const logFn = opts.log || defaultLog;
const errors = [];

// If nodeAddress specified, try it first (skip circuit breaker check for explicit choice)
Expand Down Expand Up @@ -2465,7 +2471,7 @@ export async function recoverSession(opts) {
if (!opts?.nodeAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.nodeAddress');
if (!opts?.mnemonic) throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'recoverSession requires opts.mnemonic');

const logFn = opts.log || console.log;
const logFn = opts.log || defaultLog;
const onProgress = opts.onProgress || null;
const sessionId = BigInt(opts.sessionId);
const timeouts = { ...DEFAULT_TIMEOUTS, ...opts.timeouts };
Expand Down
Loading