@whook/whook
Version:
Build strong and efficient REST web services.
223 lines (208 loc) • 6.22 kB
text/typescript
import { autoService, location } from 'knifecycle';
import { type LogService } from 'common-services';
import { type JsonValue } from 'type-fest';
import { type WhookHeaders } from '../index.js';
export type WhookSensibleValueDescriptor = {
name: string;
pattern: RegExp;
clearIndices: number[];
};
export type WhookObfuscatorConfig = {
SHIELD_CHAR?: string;
MAX_CLEAR_CHARS?: number;
MAX_CLEAR_RATIO?: number;
SENSIBLE_PROPS?: WhookSensibleValueDescriptor[];
SENSIBLE_HEADERS?: WhookSensibleValueDescriptor[];
};
export type WhookObfuscatorDependencies = {
log?: LogService;
} & WhookObfuscatorConfig;
export type WhookObfuscatorService = {
obfuscate: (secret: string) => string;
obfuscateSensibleProps: (
propValue: JsonValue,
propName?: string,
) => JsonValue;
obfuscateSensibleHeaders: (headers: WhookHeaders) => WhookHeaders;
};
export default location(autoService(initObfuscator), import.meta.url);
const noop = () => undefined;
const DEFAULT_MAX_CLEAR_CHARS = 6;
const DEFAULT_MAX_CLEAR_RATIO = 4;
const DEFAULT_SHIELD_CHAR = '🛡';
const DEFAULT_SENSIBLE_HEADERS: WhookSensibleValueDescriptor[] = [
{
name: 'authorization',
pattern: /^(Bearer |Basic )(.*)$/i,
clearIndices: [0],
},
{ name: 'cookie', pattern: /^(.*)$/i, clearIndices: [] },
{
name: 'set-cookie',
pattern: /^([^=]+=)([^;]*)(.*)$/i,
clearIndices: [0, 2],
},
];
const DEFAULT_SENSIBLE_PROPS: WhookSensibleValueDescriptor[] = [
{
name: 'access_token',
pattern: /^(.*)$/i,
clearIndices: [0],
},
{
name: 'refresh_token',
pattern: /^(.*)$/i,
clearIndices: [0],
},
{
name: 'password',
pattern: /^(.*)$/i,
clearIndices: [],
},
{
name: 'token',
pattern: /^(.*)$/i,
clearIndices: [],
},
...DEFAULT_SENSIBLE_HEADERS,
];
/**
* Obfuscate sensible information.
* @param {Object} services
* The service depend on
* @param {Object} [services.SHIELD_CHAR]
* The char for replacing sensible information
* @param {Object} [services.MAX_CLEAR_CHARS]
* The maximum clear chars to display
* @param {Object} [services.MAX_CLEAR_RATIO]
* The maximum clear chars ratio to display
* @param {Object} [services.SENSIBLE_PROPS]
* Sensible properties names
* @param {Object} [services.SENSIBLE_HEADERS]
* Sensible headers names
* @return {Promise<Object>}
* A promise of an object containing the gathered constants.
* @example
* import { initObfuscator } from '@whook/whook';
* import { alsoInject } from 'knifecycle';
* import { log } from 'node:console';
*
* const obfuscator = await initObfuscator();
*
* log(obfuscator('my very secret information!));
* // my ...on!
*/
async function initObfuscator({
SHIELD_CHAR = DEFAULT_SHIELD_CHAR,
MAX_CLEAR_CHARS = DEFAULT_MAX_CLEAR_CHARS,
MAX_CLEAR_RATIO = DEFAULT_MAX_CLEAR_RATIO,
SENSIBLE_PROPS = DEFAULT_SENSIBLE_PROPS,
SENSIBLE_HEADERS = DEFAULT_SENSIBLE_HEADERS,
log = noop,
}: WhookObfuscatorDependencies): Promise<WhookObfuscatorService> {
log('debug', '🕶️ - Initializing the obfuscator service.');
return {
obfuscate,
obfuscateSensibleProps,
obfuscateSensibleHeaders,
};
function obfuscate(secret) {
const numClearChars = Math.min(
MAX_CLEAR_CHARS,
Math.floor(secret.length / MAX_CLEAR_RATIO),
);
if (numClearChars <= 1) {
return SHIELD_CHAR;
}
return (
secret.slice(0, Math.ceil(numClearChars / 2)) +
'...' +
secret.slice(secret.length - Math.floor(numClearChars / 2))
);
}
function selectivelyObfuscateAll(
pattern: RegExp,
clearIndices: number[],
values: string | string[],
) {
return values instanceof Array
? values.map((value) =>
selectivelyObfuscate(pattern, clearIndices, value),
)
: selectivelyObfuscate(pattern, clearIndices, values);
}
function selectivelyObfuscate(
pattern: RegExp,
clearIndices: number[],
value: string,
) {
// SECURITY: Here, we first test the pattern to ensure
// the selective obfuscation will work, if not, we obfuscate
// the whole value to default to security
return 'string' !== typeof value
? obfuscate('')
: pattern.test(value)
? value.replace(pattern, (...args) => {
return args
.slice(1, -2)
.map((value, index) =>
clearIndices.includes(index) ? value : obfuscate(value),
)
.join('');
})
: obfuscate(value);
}
function obfuscateSensibleHeaders(headers: WhookHeaders): WhookHeaders {
return Object.keys(headers).reduce((finalHeaders, headerName) => {
const sensibleHeader = SENSIBLE_HEADERS.find(
(sensibleHeader) =>
sensibleHeader.name.toLowerCase() === headerName.toLowerCase(),
);
return {
...finalHeaders,
[headerName]: sensibleHeader
? selectivelyObfuscateAll(
sensibleHeader.pattern,
sensibleHeader.clearIndices,
headers[headerName],
)
: headers[headerName],
};
}, {});
}
function obfuscateSensibleProps(
propValue: JsonValue,
propName = '_',
): JsonValue {
if (propValue instanceof Array) {
return propValue.map((value) => obfuscateSensibleProps(value, propName));
} else if (typeof propValue === 'object' && propValue !== null) {
return Object.keys(propValue).reduce(
(newObject, key) => ({
...newObject,
[key]: obfuscateSensibleProps(propValue[key] as JsonValue, key),
}),
{},
);
} else if (
typeof propValue === 'boolean' ||
typeof propValue === 'string' ||
typeof propValue === 'number'
) {
const sensibleProp = SENSIBLE_PROPS.find(
(sensibleProp) =>
sensibleProp.name.toLowerCase() === propName.toLowerCase(),
);
return sensibleProp
? selectivelyObfuscate(
sensibleProp.pattern,
sensibleProp.clearIndices,
propValue.toString(),
)
: propValue;
} else if (null === propValue) {
return null;
}
return null;
}
}