From bd8703e8717e50abec85a01fec48a5e6c2e85727 Mon Sep 17 00:00:00 2001 From: Juan Picado Date: Sun, 5 May 2024 16:53:28 +0200 Subject: [PATCH] feat: add migrateToSecureLegacySignature property (#4621) * feat: add migrateToSecureLegacySignature property * Update config.ts * changeset * Update ci.yml * Update config.spec.ts --- .changeset/wet-balloons-give.md | 10 ++ .github/workflows/ci.yml | 2 +- .github/workflows/website.yml | 3 +- docs/env.variables.md | 7 +- packages/auth/src/auth.ts | 18 +-- packages/auth/src/signature.ts | 66 ---------- packages/auth/src/utils.ts | 32 +++-- packages/auth/test/auth.spec.ts | 19 +-- packages/config/src/config.ts | 114 ++++++++++++------ packages/config/src/security.ts | 1 + packages/config/src/token.ts | 2 + packages/config/test/config.spec.ts | 84 +++++++++++-- packages/core/core/src/warning-utils.ts | 35 +++--- packages/core/types/src/configuration.ts | 5 +- packages/node-api/test/run-server.spec.ts | 1 - packages/signature/package.json | 1 - packages/signature/src/index.ts | 2 - .../signature/src/legacy-signature/index.ts | 71 +++++++++-- .../legacy-backward-compatible.ts | 32 ----- .../src/legacy-signature/legacy-crypto.ts | 50 -------- packages/signature/src/signature.ts | 7 +- ...ken-deprecated-backward-compatible.spec.ts | 23 ---- .../test/legacy-token-deprecated.spec.ts | 14 ++- .../tools/helpers/src/initializeServer.ts | 4 +- pnpm-lock.yaml | 6 +- website/docs/config.md | 78 ++++++------ website/versioned_docs/version-5.x/config.md | 93 +++++++++++--- website/versioned_docs/version-6.x/config.md | 87 ++++++------- 28 files changed, 458 insertions(+), 409 deletions(-) create mode 100644 .changeset/wet-balloons-give.md delete mode 100644 packages/auth/src/signature.ts delete mode 100644 packages/signature/src/legacy-signature/legacy-backward-compatible.ts delete mode 100644 packages/signature/src/legacy-signature/legacy-crypto.ts delete mode 100644 packages/signature/test/legacy-token-deprecated-backward-compatible.spec.ts diff --git a/.changeset/wet-balloons-give.md b/.changeset/wet-balloons-give.md new file mode 100644 index 000000000..6ea3f64ce --- /dev/null +++ b/.changeset/wet-balloons-give.md @@ -0,0 +1,10 @@ +--- +'@verdaccio/types': minor +'@verdaccio/core': minor +'@verdaccio/signature': minor +'@verdaccio/node-api': minor +'@verdaccio/config': minor +'@verdaccio/auth': minor +--- + +feat: add migrateToSecureLegacySignature and remove enhancedLegacySignature property diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9527a016..c98d625de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - node_version: [18, 20, 21] + node_version: [18, 20, 21, 22] name: ${{ matrix.os }} / Node ${{ matrix.node_version }} runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index b838b01d6..f0fd9ce68 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -2,8 +2,6 @@ name: Verdaccio Website CI on: workflow_dispatch: - schedule: - - cron: '0 0 * * *' permissions: contents: read # to fetch code (actions/checkout) @@ -69,6 +67,7 @@ jobs: CONTEXT: production run: pnpm --filter @verdaccio/website netlify:build - name: Deploy to Netlify + if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'workflow_dispatch' env: NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} diff --git a/docs/env.variables.md b/docs/env.variables.md index 0c8a07fa9..9171a9e19 100644 --- a/docs/env.variables.md +++ b/docs/env.variables.md @@ -5,12 +5,13 @@ internal features. #### VERDACCIO_LEGACY_ALGORITHM -Allows to define the specific algorithm for the token -signature which by default is `aes-256-ctr` +Allows to define the specific algorithm for the token signature which by default is `aes-256-ctr`. The algorithm must be supported by `crypto.createCipheriv` and `crypto.createDecipheriv`. +Read more here: https://nodejs.org/api/crypto.html#crypto_crypto_createcipheriv_algorithm_key_iv_options #### VERDACCIO_LEGACY_ENCRYPTION_KEY -By default, the token stores in the database, but using this variable allows to get it from memory +By default, the token stores in the database, but using this variable allows to get it from memory, the length must be 32 characters otherwise will throw an error. +Read more here: https://nodejs.org/api/crypto.html#crypto_crypto_createcipheriv_algorithm_key_iv_options #### VERDACCIO_PUBLIC_URL diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 97ef9de81..e08ee157e 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -13,7 +13,6 @@ import { pluginUtils, warningUtils, } from '@verdaccio/core'; -import '@verdaccio/core'; import { asyncLoadPlugin } from '@verdaccio/loaders'; import { logger } from '@verdaccio/logger'; import { @@ -21,6 +20,7 @@ import { aesEncryptDeprecated, parseBasicPayload, signPayload, + utils as signatureUtils, } from '@verdaccio/signature'; import { AllowAccess, @@ -481,14 +481,9 @@ class Auth implements IAuthMiddleware, TokenEncryption, pluginUtils.IBasicAuth { next: Function ): void { debug('handle legacy api middleware'); - debug('api middleware secret %o', typeof secret === 'string'); + debug('api middleware has a secret? %o', typeof secret === 'string'); debug('api middleware authorization %o', typeof authorization === 'string'); - const credentials: any = getMiddlewareCredentials( - security, - secret, - authorization, - this.config?.getEnhancedLegacySignature() - ); + const credentials: any = getMiddlewareCredentials(security, secret, authorization); debug('api middleware credentials %o', credentials?.name); if (credentials) { const { user, password } = credentials; @@ -588,13 +583,12 @@ class Auth implements IAuthMiddleware, TokenEncryption, pluginUtils.IBasicAuth { * Encrypt a string. */ public aesEncrypt(value: string): string | void { - // enhancedLegacySignature enables modern aes192 algorithm signature - if (this.config?.getEnhancedLegacySignature()) { - debug('signing with enhaced aes legacy'); + if (this.secret.length === signatureUtils.TOKEN_VALID_LENGTH) { + debug('signing with enhanced aes legacy'); const token = aesEncrypt(value, this.secret); return token; } else { - debug('signing with enhaced aes deprecated legacy'); + debug('signing with enhanced aes deprecated legacy'); // deprecated aes (legacy) signature, only must be used for legacy version const token = aesEncryptDeprecated(Buffer.from(value), this.secret).toString('base64'); return token; diff --git a/packages/auth/src/signature.ts b/packages/auth/src/signature.ts deleted file mode 100644 index 9a0f548a0..000000000 --- a/packages/auth/src/signature.ts +++ /dev/null @@ -1,66 +0,0 @@ -import buildDebug from 'debug'; -import _ from 'lodash'; - -import { TOKEN_BASIC, TOKEN_BEARER } from '@verdaccio/core'; -import { aesDecrypt, parseBasicPayload } from '@verdaccio/signature'; -import { Security } from '@verdaccio/types'; - -import { AuthMiddlewarePayload } from './types'; -import { - convertPayloadToBase64, - isAESLegacy, - parseAuthTokenHeader, - verifyJWTPayload, -} from './utils'; - -const debug = buildDebug('verdaccio:auth:utils'); - -export function parseAESCredentials(authorizationHeader: string, secret: string) { - debug('parseAESCredentials'); - const { scheme, token } = parseAuthTokenHeader(authorizationHeader); - - // basic is deprecated and should not be enforced - // basic is currently being used for functional test - if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) { - debug('legacy header basic'); - const credentials = convertPayloadToBase64(token).toString(); - - return credentials; - } else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) { - debug('legacy header bearer'); - const credentials = aesDecrypt(token, secret); - - return credentials; - } -} - -export function getMiddlewareCredentials( - security: Security, - secretKey: string, - authorizationHeader: string -): AuthMiddlewarePayload { - debug('getMiddlewareCredentials'); - // comment out for debugging purposes - if (isAESLegacy(security)) { - debug('is legacy'); - const credentials = parseAESCredentials(authorizationHeader, secretKey); - if (!credentials) { - debug('parse legacy credentials failed'); - return; - } - - const parsedCredentials = parseBasicPayload(credentials); - if (!parsedCredentials) { - debug('parse legacy basic payload credentials failed'); - return; - } - - return parsedCredentials; - } - const { scheme, token } = parseAuthTokenHeader(authorizationHeader); - - debug('is jwt'); - if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) { - return verifyJWTPayload(token, secretKey); - } -} diff --git a/packages/auth/src/utils.ts b/packages/auth/src/utils.ts index 861c8c999..fa5929c8b 100644 --- a/packages/auth/src/utils.ts +++ b/packages/auth/src/utils.ts @@ -40,12 +40,8 @@ export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHead return { scheme, token }; } -export function parseAESCredentials( - authorizationHeader: string, - secret: string, - enhanced: boolean -) { - debug('parseAESCredentials'); +export function parseAESCredentials(authorizationHeader: string, secret: string) { + debug('parseAESCredentials init'); const { scheme, token } = parseAuthTokenHeader(authorizationHeader); // basic is deprecated and should not be enforced @@ -57,27 +53,29 @@ export function parseAESCredentials( return credentials; } else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) { debug('legacy header bearer'); - debug('legacy header enhanced?', enhanced); - const credentials = enhanced - ? aesDecrypt(token.toString(), secret) - : // FUTURE: once deprecated legacy is removed this logic won't be longer need it - aesDecryptDeprecated(convertPayloadToBase64(token), secret).toString('utf-8'); - - return credentials; + debug('secret length %o', secret.length); + const isLegacyUnsecure = secret.length > 32; + debug('is legacy unsecure %o', isLegacyUnsecure); + if (isLegacyUnsecure) { + debug('legacy unsecure enabled'); + return aesDecryptDeprecated(convertPayloadToBase64(token), secret).toString('utf-8'); + } else { + debug('legacy secure enabled'); + return aesDecrypt(token.toString(), secret); + } } } export function getMiddlewareCredentials( security: Security, secretKey: string, - authorizationHeader: string, - enhanced: boolean = true + authorizationHeader: string ): AuthMiddlewarePayload { - debug('getMiddlewareCredentials'); + debug('getMiddlewareCredentials init'); // comment out for debugging purposes if (isAESLegacy(security)) { debug('is legacy'); - const credentials = parseAESCredentials(authorizationHeader, secretKey, enhanced); + const credentials = parseAESCredentials(authorizationHeader, secretKey); if (!credentials) { debug('parse legacy credentials failed'); return; diff --git a/packages/auth/test/auth.spec.ts b/packages/auth/test/auth.spec.ts index af362ca2c..1ad085342 100644 --- a/packages/auth/test/auth.spec.ts +++ b/packages/auth/test/auth.spec.ts @@ -601,16 +601,14 @@ describe('AuthTest', () => { }); }); - describe('deprecated legacy handling forceEnhancedLegacySignature=false', () => { + describe('deprecated legacy handling', () => { test('should handle valid auth token', async () => { const payload = 'juan:password'; // const token = await signPayload(remoteUser, '12345'); - const config: Config = new AppConfig( - { ...authProfileConf }, - { forceEnhancedLegacySignature: false } - ); + const config: Config = new AppConfig({ ...authProfileConf }); // intended to force key generator (associated with mocks above) - config.checkSecretKey(undefined); + // 64 characters secret long + config.checkSecretKey('35fabdd29b820d39125e76e6d85cc294'); const auth = new Auth(config); await auth.init(); const token = auth.aesEncrypt(payload) as string; @@ -624,10 +622,7 @@ describe('AuthTest', () => { test('should handle invalid auth token', async () => { const payload = 'juan:password'; - const config: Config = new AppConfig( - { ...authPluginFailureConf }, - { forceEnhancedLegacySignature: false } - ); + const config: Config = new AppConfig({ ...authPluginFailureConf }); // intended to force key generator (associated with mocks above) config.checkSecretKey(undefined); const auth = new Auth(config); @@ -691,8 +686,7 @@ describe('AuthTest', () => { { ...authProfileConf, ...{ security: { api: { jwt: { sign: { expiresIn: '29d' } } } } }, - }, - { forceEnhancedLegacySignature: false } + } ); // intended to force key generator (associated with mocks above) config.checkSecretKey(undefined); @@ -700,7 +694,6 @@ describe('AuthTest', () => { await auth.init(); const token = (await auth.jwtEncrypt( createRemoteUser('jwt_user', [ROLES.ALL]), - // @ts-expect-error config.security.api.jwt.sign )) as string; const app = await getServer(auth); diff --git a/packages/config/src/config.ts b/packages/config/src/config.ts index 37b2d978b..5df030538 100644 --- a/packages/config/src/config.ts +++ b/packages/config/src/config.ts @@ -36,6 +36,13 @@ export const defaultUserRateLimiting = { max: 1000, }; +export function isNodeVersionGreaterThan21() { + const [major, minor] = process.versions.node.split('.').map(Number); + return major > 21 || (major === 21 && minor >= 0); +} + +const TOKEN_VALID_LENGTH = 32; + /** * Coordinates the application configuration */ @@ -56,21 +63,20 @@ class Config implements AppConfig { public plugins: string | void | null; public security: Security; public serverSettings: ServerSettingsConf; + private configOverrideOptions: { forceMigrateToSecureLegacySignature: boolean }; // @ts-ignore public secret: string; public flags: FlagsConfig; public userRateLimit: RateLimit; - private configOptions: { forceEnhancedLegacySignature: boolean }; public constructor( config: ConfigYaml & { config_path: string }, // forceEnhancedLegacySignature is a property that // allows switch a new legacy aes signature token signature // for older versions do not want to have this new signature model // this property must be false - configOptions = { forceEnhancedLegacySignature: true } + configOverrideOptions = { forceMigrateToSecureLegacySignature: true } ) { const self = this; - this.configOptions = configOptions; this.storage = process.env.VERDACCIO_STORAGE_PATH || config.storage; if (!config.configPath) { // backport self_path for previous to version 6 @@ -80,11 +86,21 @@ class Config implements AppConfig { throw new Error('configPath property is required'); } } + this.configOverrideOptions = configOverrideOptions; this.configPath = config.configPath; this.self_path = this.configPath; debug('config path: %s', this.configPath); this.plugins = config.plugins; - this.security = _.merge(defaultSecurity, config.security); + this.security = _.merge( + // override the default security configuration via constructor + _.merge(defaultSecurity, { + api: { + migrateToSecureLegacySignature: + this.configOverrideOptions.forceMigrateToSecureLegacySignature, + }, + }), + config.security + ); this.serverSettings = serverSettings; this.flags = { searchRemote: config.flags?.searchRemote ?? true, @@ -135,14 +151,8 @@ class Config implements AppConfig { } } - public getEnhancedLegacySignature() { - if (typeof this?.security.enhancedLegacySignature !== 'undefined') { - if (this.security.enhancedLegacySignature === true) { - return true; - } - return false; - } - return this.configOptions.forceEnhancedLegacySignature; + public getMigrateToSecureLegacySignature() { + return this.security.api.migrateToSecureLegacySignature; } public getConfigPath() { @@ -158,36 +168,70 @@ class Config implements AppConfig { } /** - * Store or create whether receive a secret key + * Verify if the secret complies with the required structure + * - If the secret is not provided, it will generate a new one + * - For any Node.js version the new secret will be 32 characters long (to allow compatibility with modern Node.js versions) + * - If the secret is provided: + * - If Node.js 22 or higher, the secret must be 32 characters long thus the application will fail on startup + * - If Node.js 21 or lower, the secret will be used as is but will display a deprecation warning + * - If the property `security.api.migrateToSecureLegacySignature` is provided and set to true, the secret will be + * generated with the new signature model * @secret external secret key */ public checkSecretKey(secret?: string): string { - debug('check secret key'); + debug('checking secret key init'); if (typeof secret === 'string' && _.isEmpty(secret) === false) { + debug('checking secret key length %s', secret.length); + if (secret.length > TOKEN_VALID_LENGTH) { + if (isNodeVersionGreaterThan21()) { + debug('is node version greater than 21'); + if (this.getMigrateToSecureLegacySignature() === true) { + this.secret = generateRandomSecretKey(); + debug('rewriting secret key with length %s', this.secret.length); + return this.secret; + } + // oops, user needs to generate a new secret key + debug( + 'secret does not comply with the required length, current length %d, application will fail on startup', + secret.length + ); + throw new Error( + `Invalid storage secret key length, must be 32 characters long but is ${secret.length}. + The secret length in Node.js 22 or higher must be 32 characters long. Please consider generate a new one. + Learn more at https://verdaccio.org/docs/configuration/#.verdaccio-db` + ); + } else { + debug('is node version lower than 22'); + if (this.getMigrateToSecureLegacySignature() === true) { + this.secret = generateRandomSecretKey(); + debug('rewriting secret key with length %s', this.secret.length); + return this.secret; + } + debug('triggering deprecation warning for secret key length %s', secret.length); + // still using Node.js versions previous to 22, but we need to emit a deprecation warning + // deprecation warning, secret key is too long and must be 32 + // this will be removed in the next major release and will produce an error + warningUtils.emit(Codes.VERWAR007); + this.secret = secret; + return this.secret; + } + } else if (secret.length === TOKEN_VALID_LENGTH) { + debug('detected valid secret key length %s', secret.length); + this.secret = secret; + return this.secret; + } + debug('reusing previous key with length %s', secret.length); this.secret = secret; - debug('reusing previous key'); - return secret; - } - // generate a new a secret key - // FUTURE: this might be an external secret key, perhaps within config file? - debug('generating a new secret key'); - - if (this.getEnhancedLegacySignature()) { - debug('key generated with "enhanced" legacy signature user config'); - this.secret = generateRandomSecretKey(); + return this.secret; } else { - debug('key generated with legacy signature user config'); - this.secret = generateRandomHexString(32); - } - // set this to false allow use old token signature and is not recommended - // only use for migration reasons, major release will remove this property and - // set it by default - if (this.security?.enhancedLegacySignature === false) { - warningUtils.emit(Codes.VERWAR005); - } + // generate a new a secret key + // FUTURE: this might be an external secret key, perhaps within config file? + debug('generating a new secret key'); + this.secret = generateRandomSecretKey(); + debug('generated a new secret key length %s', this.secret?.length); - debug('generated a new secret key length %s', this.secret?.length); - return this.secret; + return this.secret; + } } } diff --git a/packages/config/src/security.ts b/packages/config/src/security.ts index 493f4901e..2a6bb6937 100644 --- a/packages/config/src/security.ts +++ b/packages/config/src/security.ts @@ -13,6 +13,7 @@ const defaultWebTokenOptions: JWTOptions = { const defaultApiTokenConf: APITokenOptions = { legacy: true, + migrateToSecureLegacySignature: true, }; export const defaultSecurity: Security = { diff --git a/packages/config/src/token.ts b/packages/config/src/token.ts index c7e986610..ec0465af1 100644 --- a/packages/config/src/token.ts +++ b/packages/config/src/token.ts @@ -1,9 +1,11 @@ import { randomBytes } from 'crypto'; +// TODO: code duplicated at @verdaccio/signature export const TOKEN_VALID_LENGTH = 32; /** * Secret key must have 32 characters. + * // TODO: code duplicated at @verdaccio/signature */ export function generateRandomSecretKey(): string { return randomBytes(TOKEN_VALID_LENGTH).toString('base64').substring(0, TOKEN_VALID_LENGTH); diff --git a/packages/config/test/config.spec.ts b/packages/config/test/config.spec.ts index 1fe6b9595..babff90ef 100644 --- a/packages/config/test/config.spec.ts +++ b/packages/config/test/config.spec.ts @@ -6,9 +6,12 @@ import { DEFAULT_REGISTRY, DEFAULT_UPLINK, ROLES, + TOKEN_VALID_LENGTH, WEB_TITLE, defaultSecurity, + generateRandomSecretKey, getDefaultConfig, + isNodeVersionGreaterThan21, parseConfigFile, } from '../src'; import { parseConfigurationFile } from './utils'; @@ -19,6 +22,8 @@ const resolveConf = (conf) => { return path.join(__dirname, `../src/conf/${name}${ext.startsWith('.') ? ext : '.yaml'}`); }; +const itif = (condition) => (condition ? it : it.skip); + const checkDefaultUplink = (config) => { expect(_.isObject(config.uplinks[DEFAULT_UPLINK])).toBeTruthy(); expect(config.uplinks[DEFAULT_UPLINK].url).toMatch(DEFAULT_REGISTRY); @@ -94,32 +99,85 @@ describe('check basic content parsed file', () => { describe('checkSecretKey', () => { test('with default.yaml and pre selected secret', () => { const config = new Config(parseConfigFile(resolveConf('default'))); - expect(config.checkSecretKey('12345')).toEqual('12345'); + expect(config.checkSecretKey(generateRandomSecretKey())).toHaveLength(TOKEN_VALID_LENGTH); }); test('with default.yaml and void secret', () => { const config = new Config(parseConfigFile(resolveConf('default'))); - expect(typeof config.checkSecretKey() === 'string').toBeTruthy(); + const secret = config.checkSecretKey(); + expect(typeof secret === 'string').toBeTruthy(); + expect(secret).toHaveLength(TOKEN_VALID_LENGTH); }); - test('with default.yaml and emtpy string secret', () => { + test('with default.yaml and empty string secret', () => { const config = new Config(parseConfigFile(resolveConf('default'))); - expect(typeof config.checkSecretKey('') === 'string').toBeTruthy(); + const secret = config.checkSecretKey(''); + expect(typeof secret === 'string').toBeTruthy(); + expect(secret).toHaveLength(TOKEN_VALID_LENGTH); }); - test('with enhanced legacy signature', () => { + test('with default.yaml and valid string secret length', () => { const config = new Config(parseConfigFile(resolveConf('default'))); - config.security.enhancedLegacySignature = true; - expect(typeof config.checkSecretKey() === 'string').toBeTruthy(); - expect(config.secret.length).toBe(32); + expect(typeof config.checkSecretKey(generateRandomSecretKey()) === 'string').toBeTruthy(); }); - test('without enhanced legacy signature', () => { - const config = new Config(parseConfigFile(resolveConf('default'))); - config.security.enhancedLegacySignature = false; - expect(typeof config.checkSecretKey() === 'string').toBeTruthy(); - expect(config.secret.length).toBe(64); + test('with default.yaml migrate a valid string secret length', () => { + const config = new Config(parseConfigFile(resolveConf('default')), { + forceMigrateToSecureLegacySignature: true, + }); + expect( + // 64 characters secret long + config.checkSecretKey('b4982dbb0108531fafb552374d7e83724b6458a2b3ffa97ad0edb899bdaefc4a') + ).toHaveLength(TOKEN_VALID_LENGTH); }); + + // only runs on Node.js 22 or higher + itif(isNodeVersionGreaterThan21())('with enhanced legacy signature Node 22 or higher', () => { + const config = new Config(parseConfigFile(resolveConf('default')), { + forceMigrateToSecureLegacySignature: false, + }); + // eslint-disable-next-line jest/no-standalone-expect + expect(() => + // 64 characters secret long + config.checkSecretKey('b4982dbb0108531fafb552374d7e83724b6458a2b3ffa97ad0edb899bdaefc4a') + ).toThrow(); + }); + + itif(isNodeVersionGreaterThan21())('with enhanced legacy signature Node 22 or higher', () => { + const config = new Config(parseConfigFile(resolveConf('default')), { + forceMigrateToSecureLegacySignature: false, + }); + config.security.api.migrateToSecureLegacySignature = true; + // eslint-disable-next-line jest/no-standalone-expect + expect( + config.checkSecretKey('b4982dbb0108531fafb552374d7e83724b6458a2b3ffa97ad0edb899bdaefc4a') + ).toHaveLength(TOKEN_VALID_LENGTH); + }); + + itif(isNodeVersionGreaterThan21() === false)( + 'with old unsecure legacy signature Node 21 or lower', + () => { + const config = new Config(parseConfigFile(resolveConf('default'))); + config.security.api.migrateToSecureLegacySignature = false; + // 64 characters secret long + // eslint-disable-next-line jest/no-standalone-expect + expect( + config.checkSecretKey('b4982dbb0108531fafb552374d7e83724b6458a2b3ffa97ad0edb899bdaefc4a') + ).toHaveLength(64); + } + ); + + test('with migration to new legacy signature Node 21 or lower', () => { + const config = new Config(parseConfigFile(resolveConf('default'))); + config.security.api.migrateToSecureLegacySignature = true; + // 64 characters secret long + // eslint-disable-next-line jest/no-standalone-expect + expect( + config.checkSecretKey('b4982dbb0108531fafb552374d7e83724b6458a2b3ffa97ad0edb899bdaefc4a') + ).toHaveLength(TOKEN_VALID_LENGTH); + }); + + test.todo('test emit warning with secret key'); }); describe('getMatchedPackagesSpec', () => { diff --git a/packages/core/core/src/warning-utils.ts b/packages/core/core/src/warning-utils.ts index e7e84b905..4e96f35b5 100644 --- a/packages/core/core/src/warning-utils.ts +++ b/packages/core/core/src/warning-utils.ts @@ -9,17 +9,13 @@ export enum Codes { VERWAR002 = 'VERWAR002', VERWAR003 = 'VERWAR003', VERWAR004 = 'VERWAR004', - VERWAR005 = 'VERWAR005', // deprecation warnings VERDEP003 = 'VERDEP003', VERWAR006 = 'VERWAR006', + VERWAR007 = 'VERWAR007', } -warningInstance.create( - verdaccioWarning, - Codes.VERWAR002, - `The configuration property "logs" has been deprecated, please rename to "log" for future compatibility` -); +/* general warnings */ warningInstance.create( verdaccioWarning, @@ -27,6 +23,12 @@ warningInstance.create( `Verdaccio doesn't need superuser privileges. don't run it under root` ); +warningInstance.create( + verdaccioWarning, + Codes.VERWAR002, + `The configuration property "logs" has been deprecated, please rename to "log" for future compatibility` +); + warningInstance.create( verdaccioWarning, Codes.VERWAR003, @@ -42,23 +44,26 @@ https://verdaccio.org/docs/en/configuration#listen-port` ); warningInstance.create( - verdaccioWarning, - Codes.VERWAR005, - 'disable enhanced legacy signature is considered a security risk, please reconsider enable it' + verdaccioDeprecation, + Codes.VERWAR006, + 'the auth plugin method "add_user" in the auth plugin is deprecated and will be removed in next major release, rename to "adduser"' ); +warningInstance.create( + verdaccioDeprecation, + Codes.VERWAR007, + `the secret length is too long, it must be 32 characters long, please consider generate a new one + Learn more at https://verdaccio.org/docs/configuration/#.verdaccio-db` +); + +/* deprecation warnings */ + warningInstance.create( verdaccioDeprecation, Codes.VERDEP003, 'multiple addresses will be deprecated in the next major, only use one' ); -warningInstance.create( - verdaccioDeprecation, - Codes.VERWAR006, - 'the auth plugin method "add_user" in the auth plugin is deprecated and will be removed in next major release, rename to "adduser"' -); - export function emit(code: string, a?: string, b?: string, c?: string) { warningInstance.emit(code, a, b, c); } diff --git a/packages/core/types/src/configuration.ts b/packages/core/types/src/configuration.ts index bae1cf2a1..8fdc403d8 100644 --- a/packages/core/types/src/configuration.ts +++ b/packages/core/types/src/configuration.ts @@ -182,11 +182,14 @@ export interface JWTVerifyOptions { export interface APITokenOptions { legacy: boolean; + /** + * Temporary flag to allow migration to the new legacy signature + */ + migrateToSecureLegacySignature: boolean; jwt?: JWTOptions; } export interface Security { - enhancedLegacySignature?: boolean; web: JWTOptions; api: APITokenOptions; } diff --git a/packages/node-api/test/run-server.spec.ts b/packages/node-api/test/run-server.spec.ts index 095723642..d8451034c 100644 --- a/packages/node-api/test/run-server.spec.ts +++ b/packages/node-api/test/run-server.spec.ts @@ -15,7 +15,6 @@ describe('startServer via API', () => { }); test('should fail on start with null as entry', async () => { - // @ts-expect-error await expect(runServer(null)).rejects.toThrow(); }); }); diff --git a/packages/signature/package.json b/packages/signature/package.json index 544eba787..4c75341e0 100644 --- a/packages/signature/package.json +++ b/packages/signature/package.json @@ -39,7 +39,6 @@ }, "dependencies": { "jsonwebtoken": "9.0.2", - "evp_bytestokey": "1.0.3", "debug": "4.3.4" }, "devDependencies": { diff --git a/packages/signature/src/index.ts b/packages/signature/src/index.ts index c2f755890..68d5476be 100644 --- a/packages/signature/src/index.ts +++ b/packages/signature/src/index.ts @@ -2,8 +2,6 @@ export { aesDecryptDeprecated, aesEncryptDeprecated, generateRandomSecretKeyDeprecated, - aesDecryptDeprecatedBackwardCompatible, - aesEncryptDeprecatedBackwardCompatible, } from './legacy-signature'; export { aesDecrypt, aesEncrypt } from './signature'; diff --git a/packages/signature/src/legacy-signature/index.ts b/packages/signature/src/legacy-signature/index.ts index c4745fd05..4c482cf13 100644 --- a/packages/signature/src/legacy-signature/index.ts +++ b/packages/signature/src/legacy-signature/index.ts @@ -1,13 +1,58 @@ -export { - aesDecryptDeprecated, - aesEncryptDeprecated, - generateRandomSecretKeyDeprecated, - TOKEN_VALID_LENGTH_DEPRECATED, - defaultAlgorithm, - defaultTarballHashAlgorithm, -} from './legacy-crypto'; -// Temporary export to keep backward compatibility with Node.js >= 22 -export { - aesDecryptDeprecatedBackwardCompatible, - aesEncryptDeprecatedBackwardCompatible, -} from './legacy-backward-compatible'; +import { createCipher, createDecipher } from 'crypto'; +import buildDebug from 'debug'; + +import { generateRandomHexString } from '../utils'; + +export const defaultAlgorithm = 'aes192'; +export const defaultTarballHashAlgorithm = 'sha1'; + +const debug = buildDebug('verdaccio:auth:token:legacy:deprecated'); + +/** + * + * @param buf + * @param secret + * @returns + */ +export function aesEncryptDeprecated(buf: Buffer, secret: string): Buffer { + debug('aesEncryptDeprecated init'); + debug('algorithm %o', defaultAlgorithm); + // deprecated (it will be removed in Verdaccio 6), it is a breaking change + // https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options + // https://www.grainger.xyz/changing-from-cipher-to-cipheriv/ + const c = createCipher(defaultAlgorithm, secret); + const b1 = c.update(buf); + const b2 = c.final(); + debug('deprecated legacy token generated successfully'); + return Buffer.concat([b1, b2]); +} + +/** + * + * @param buf + * @param secret + * @returns + */ +export function aesDecryptDeprecated(buf: Buffer, secret: string): Buffer { + try { + debug('aesDecryptDeprecated init'); + // https://nodejs.org/api/crypto.html#crypto_crypto_createdecipher_algorithm_password_options + // https://www.grainger.xyz/changing-from-cipher-to-cipheriv/ + const c = createDecipher(defaultAlgorithm, secret); + const b1 = c.update(buf); + const b2 = c.final(); + debug('deprecated legacy token payload decrypted successfully'); + return Buffer.concat([b1, b2]); + } catch (_) { + return Buffer.alloc(0); + } +} + +export const TOKEN_VALID_LENGTH_DEPRECATED = 64; + +/** + * Generate a secret key of 64 characters. + */ +export function generateRandomSecretKeyDeprecated(): string { + return generateRandomHexString(6); +} diff --git a/packages/signature/src/legacy-signature/legacy-backward-compatible.ts b/packages/signature/src/legacy-signature/legacy-backward-compatible.ts deleted file mode 100644 index d3a7a2b00..000000000 --- a/packages/signature/src/legacy-signature/legacy-backward-compatible.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable new-cap */ -import { createCipheriv, createDecipheriv } from 'crypto'; -import EVP_BytesToKey from 'evp_bytestokey'; - -export const defaultAlgorithm = 'aes192'; -const KEY_SIZE = 24; - -export function aesDecryptDeprecatedBackwardCompatible(text, secret: string) { - const result = EVP_BytesToKey( - secret, - null, - KEY_SIZE * 8, // byte to bit size - 16 - ); - - let decipher = createDecipheriv(defaultAlgorithm, result.key, result.iv); - let decrypted = decipher.update(text, 'hex', 'utf8') + decipher.final('utf8'); - return decrypted.toString(); -} - -export function aesEncryptDeprecatedBackwardCompatible(text, secret: string) { - const result = EVP_BytesToKey( - secret, - null, - KEY_SIZE * 8, // byte to bit size - 16 - ); - - const cipher = createCipheriv(defaultAlgorithm, result.key, result.iv); - const encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex'); - return encrypted.toString(); -} diff --git a/packages/signature/src/legacy-signature/legacy-crypto.ts b/packages/signature/src/legacy-signature/legacy-crypto.ts deleted file mode 100644 index 88fdd1c1d..000000000 --- a/packages/signature/src/legacy-signature/legacy-crypto.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createCipher, createDecipher } from 'crypto'; - -import { generateRandomHexString } from '../utils'; - -export const defaultAlgorithm = 'aes192'; -export const defaultTarballHashAlgorithm = 'sha1'; - -/** - * Deprecated version usage of crypto.createCipher, only useful for node.js versions < 22. - * @param buf - * @param secret - * @returns - */ -export function aesEncryptDeprecated(buf: Buffer, secret: string): Buffer { - // deprecated (it will be removed in Verdaccio 6), it is a breaking change - // https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options - // https://www.grainger.xyz/changing-from-cipher-to-cipheriv/ - const c = createCipher(defaultAlgorithm, secret); - const b1 = c.update(buf); - const b2 = c.final(); - return Buffer.concat([b1, b2]); -} - -/** - * Deprecated version usage of crypto.createCipher, only useful for node.js versions < 22. - * @param buf - * @param secret - * @returns - */ -export function aesDecryptDeprecated(buf: Buffer, secret: string): Buffer { - try { - // https://nodejs.org/api/crypto.html#crypto_crypto_createdecipher_algorithm_password_options - // https://www.grainger.xyz/changing-from-cipher-to-cipheriv/ - const c = createDecipher(defaultAlgorithm, secret); - const b1 = c.update(buf); - const b2 = c.final(); - return Buffer.concat([b1, b2]); - } catch (_) { - return Buffer.alloc(0); - } -} - -export const TOKEN_VALID_LENGTH_DEPRECATED = 64; - -/** - * Generate a secret key of 64 characters. - */ -export function generateRandomSecretKeyDeprecated(): string { - return generateRandomHexString(6); -} diff --git a/packages/signature/src/signature.ts b/packages/signature/src/signature.ts index 34da934c9..f51b3c6a7 100644 --- a/packages/signature/src/signature.ts +++ b/packages/signature/src/signature.ts @@ -19,9 +19,9 @@ const outputEncoding: BinaryToTextEncoding = 'hex'; const VERDACCIO_LEGACY_ENCRYPTION_KEY = process.env.VERDACCIO_LEGACY_ENCRYPTION_KEY; export function aesEncrypt(value: string, key: string): string | void { + debug('aesEncrypt init'); // https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options // https://www.grainger.xyz/posts/changing-from-cipher-to-cipheriv - debug('encrypt %o', value); debug('algorithm %o', defaultAlgorithm); // IV must be a buffer of length 16 const iv = randomBytes(16); @@ -42,12 +42,13 @@ export function aesEncrypt(value: string, key: string): string | void { // @ts-ignore encrypted += cipher.final(outputEncoding); const token = `${iv.toString('hex')}:${encrypted.toString()}`; - debug('token generated successfully'); + debug('legacy token generated successfully'); return Buffer.from(token).toString('base64'); } export function aesDecrypt(value: string, key: string): string | void { try { + debug('aesDecrypt init'); const buff = Buffer.from(value, 'base64'); const textParts = buff.toString().split(':'); @@ -62,7 +63,7 @@ export function aesDecrypt(value: string, key: string): string | void { // FIXME: fix type here should allow Buffer let decrypted = decipher.update(encryptedText as any, outputEncoding, inputEncoding); decrypted += decipher.final(inputEncoding); - debug('token decrypted successfully'); + debug('legacy token payload decrypted successfully'); return decrypted.toString(); } catch (_: any) { return; diff --git a/packages/signature/test/legacy-token-deprecated-backward-compatible.spec.ts b/packages/signature/test/legacy-token-deprecated-backward-compatible.spec.ts deleted file mode 100644 index 0ed1bd7c3..000000000 --- a/packages/signature/test/legacy-token-deprecated-backward-compatible.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - aesDecryptDeprecatedBackwardCompatible, - aesEncryptDeprecatedBackwardCompatible, - generateRandomSecretKeyDeprecated, -} from '../src'; - -describe('test deprecated crypto utils', () => { - test('decrypt payload flow', () => { - const secret = generateRandomSecretKeyDeprecated(); - const payload = 'juan:password'; - const token = aesEncryptDeprecatedBackwardCompatible(Buffer.from(payload), secret); - const data = aesDecryptDeprecatedBackwardCompatible(token, secret); - - expect(data.toString()).toEqual(payload.toString()); - }); - - test('crypt fails if secret is incorrect', () => { - const payload = 'juan:password'; - expect( - aesEncryptDeprecatedBackwardCompatible(Buffer.from(payload), 'fake_token').toString() - ).not.toEqual(Buffer.from(payload)); - }); -}); diff --git a/packages/signature/test/legacy-token-deprecated.spec.ts b/packages/signature/test/legacy-token-deprecated.spec.ts index cfdc37ce0..ffdea7728 100644 --- a/packages/signature/test/legacy-token-deprecated.spec.ts +++ b/packages/signature/test/legacy-token-deprecated.spec.ts @@ -1,14 +1,24 @@ +import { isNodeVersionGreaterThan21 } from '@verdaccio/config'; + import { aesDecryptDeprecated, aesEncryptDeprecated, generateRandomSecretKeyDeprecated, } from '../src'; -describe('test deprecated crypto utils', () => { +const itdescribe = (condition) => (condition ? describe : describe.skip); + +itdescribe(isNodeVersionGreaterThan21() === false)('test deprecated crypto utils', () => { + test('generateRandomSecretKeyDeprecated', () => { + expect(generateRandomSecretKeyDeprecated()).toHaveLength(12); + }); + test('decrypt payload flow', () => { - const secret = generateRandomSecretKeyDeprecated(); + const secret = '4b4512c6ce20'; const payload = 'juan:password'; const token = aesEncryptDeprecated(Buffer.from(payload), secret); + + expect(token.toString('base64')).toEqual('auizc1j3lSEd2wEB5CyGbQ=='); const data = aesDecryptDeprecated(token, secret); expect(data.toString()).toEqual(payload.toString()); diff --git a/packages/tools/helpers/src/initializeServer.ts b/packages/tools/helpers/src/initializeServer.ts index 3cc6d0029..68e67ba70 100644 --- a/packages/tools/helpers/src/initializeServer.ts +++ b/packages/tools/helpers/src/initializeServer.ts @@ -18,9 +18,7 @@ export async function initializeServer( Storage ): Promise { const app = express(); - // verdaccio next always uses forceEnhancedLegacySignature while legacy (5.x, 6.x) - // have this property false by default - const config = new Config(configName, { forceEnhancedLegacySignature: true }); + const config = new Config(configName); config.storage = path.join(os.tmpdir(), '/storage', generateRandomHexString()); // httpass would get path.basename() for configPath thus we need to create a dummy folder // to avoid conflics diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b530ae3f2..217801d33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1602,9 +1602,6 @@ importers: debug: specifier: 4.3.4 version: 4.3.4(supports-color@5.5.0) - evp_bytestokey: - specifier: 1.0.3 - version: 1.0.3 jsonwebtoken: specifier: 9.0.2 version: 9.0.2 @@ -17879,6 +17876,7 @@ packages: dependencies: md5.js: 1.3.5 safe-buffer: 5.2.1 + dev: true /exec-sh@0.3.6: resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} @@ -19273,6 +19271,7 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 safe-buffer: 5.2.1 + dev: true /hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -21929,6 +21928,7 @@ packages: hash-base: 3.1.0 inherits: 2.0.4 safe-buffer: 5.2.1 + dev: true /mdast-squeeze-paragraphs@4.0.0: resolution: {integrity: sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==} diff --git a/website/docs/config.md b/website/docs/config.md index 8fcd9a09c..2629f8bba 100644 --- a/website/docs/config.md +++ b/website/docs/config.md @@ -45,25 +45,28 @@ storage: ./storage ### The `.verdaccio-db` database {#.verdaccio-db} -:::info -Only available if user does not use a custom storage -::: +The tiny database is used to store private packages published by the user. The database is based on a JSON file that contains +the list of private packages published and the secret token used for the token signature. +It is created automatically when starting the application for the first time. -By default verdaccio uses a little database to store private packages the `storage` property is defined in the `config.yaml` file. +The location of the database is based on the `config.yaml` folder location, for instance: -The location might change based in your operative system, see [here](cli.md) more details about location of files. +If the `config.yaml` is located in `/some_local_path/config.yaml`, the database will be created in `/some_local_path/storage/.verdaccio-db`. + +_The `.verdaccio-db` file database is only available if user does not use a custom storage_, by default verdaccio uses a tiny database to store private packages the `storage` property is defined in the `config.yaml` file. +The location might change based on your operating system. [Read the CLI section](cli.md) for more details about the location of files. The structure of the database is based in JSON file, for instance: ```json { "list": ["package1", "@scope/pkg2"], - "secret": "secret_token" + "secret": "secret_token_32_characters_long" } ``` - `list`: Is an array with the list of the private packages published, any item on this list is considered being published by the user. -- `secret`: The secret field is used for verify the token signature, either for _JWT_ or legacy token signature. +- `secret`: The secret field is used for the token signature and verification, either for _JWT_ or legacy token signature. ### Plugins {#plugins} @@ -86,48 +89,39 @@ auth: ### Token signature {#token} -The default token signature is based on the Advanced Encryption Standard (AES) with the algorithm `aes192`, known as _legacy_. It's important to note that legacy tokens are not designed to expire. If expiration functionality is needed, it is recommended to use _JSON Web Tokens (JWT)_ instead. +The default token signature is based on the Advanced Encryption Standard (AES) with the algorithm `aes-256-ctr`, known as _legacy_. It's important to note that legacy tokens are not designed to expire. If expiration functionality is needed, it is recommended to use _JSON Web Tokens (JWT)_ instead. -#### Security {#security} +### Security {#security} -The security block allows you to customise the token signature. To enable a new [JWT (JSON Web Tokens)](https://jwt.io/) signature you need to add the block `jwt` to the `api` section; `web` uses `jwt` by default. +The security block permits customization of the token signature with two options. The configuration is divided into +two sections, `api` and `web`. When using JWT on `api`, it must be defined; otherwise, the legacy token signature (`aes-256-ctr`) will be utilized. -The configuration is separated in two sections, `api` and `web`. To use JWT on `api` it has to be defined, otherwise the legacy token signature (`aes192`) will be used. For JWT you might want to customize the [signature](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) and the token [verification](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) with your own properties. +#### How to the token is generated? -``` +The token signature requires a **secret token** generated by custom plugin that creates the `.verdaccio-db` database or in case a custom storage is used, +the secret token is fetched from the plugin implementation itself. In any case the _secret token_ is required to start the application. + +#### Legacy Token Signature + +The `legacy` property is used to enable the legacy token signature. **By default is enabled**. The legacy feature only applies to the API, the web UI uses JWT by default. + +```yaml +security: + api: + legacy: true # by default is true even if this section is not defined +``` + +#### JWT Token Signature + +To enable a new [JWT (JSON Web Tokens)](https://jwt.io/) signature, the `jwt` block needs to be added to the `api` section; `jwt` is utilized by default in `web`. + +By using the JWT signature is also possible to customize the [signature](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) and the token [verification](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) with your own properties. + +```yaml security: - enhancedLegacySignature: false - api: - legacy: true - jwt: - sign: - expiresIn: 29d - verify: - someProp: [value] - web: - sign: - expiresIn: 1h # 1 hour by default - verify: - someProp: [value] -``` - -#### `enhancedLegacySignature` {#enhancedLegacySignature} - -In certain instances, particularly in older installations, you might encounter the warning `[DEP0106] DeprecationWarning: crypto.createDecipher is deprecated`. in your terminal. This warning indicates that Node.js has deprecated a function utilized by the legacy signature. To address this, you can enable the enhancedLegacySignature property, which switches the legacy token signature to one based on AES-192 with an initialization vector. - -:::caution - -It is crucial to emphasize that enabling this option will lead to the invalidation of previous tokens. - -In v6.x and older versions, the property `enhancedLegacySignature` is set to `false` by default upon initialization, to change that behaviour follow as illustrated in the following example. - -:::caution - -``` -security: - enhancedLegacySignature: true api: legacy: true + migrateToSecureLegacySignature: true # will generate a new secret token if the length is 64 characters jwt: sign: expiresIn: 29d diff --git a/website/versioned_docs/version-5.x/config.md b/website/versioned_docs/version-5.x/config.md index 02781aa94..f8d1bac45 100644 --- a/website/versioned_docs/version-5.x/config.md +++ b/website/versioned_docs/version-5.x/config.md @@ -45,25 +45,61 @@ storage: ./storage ### The `.verdaccio-db` database {#.verdaccio-db} +The tiny database is used to store private packages published by the user. The database is based on a JSON file that contains +the list of private packages published and the secret token used for the token signature. +It is created automatically when starting the application for the first time. + +The location of the database is based on the `config.yaml` folder location, for instance: + +If the `config.yaml` is located in `/some_local_path/config.yaml`, the database will be created in `/some_local_path/storage/.verdaccio-db`. + :::info -Only available if user does not use a custom storage + +For users who have been using Verdaccio for an extended period and the `.verdaccio-db` file already exist the secret +may be **64 characters** long. However, for newer installations, the length will be generated as **32 characters** long. + +If the secret length is **64 characters** long: + +- For users running Verdaccio 5.x on **Node.js 22** or higher, **the application will fail to start** if the secret length **is not** 32 characters long. +- For users running Verdaccio 5.x on **Node.js 21** or lower, the application will start, but it will display a deprecation warning at the console. + +#### How to upgrade the token secret at the storage? + +:warning: **If the secret is updated will invalidate all previous generated tokens.** + +##### Option 1: Manually + +Go to the [storage location](cli.md) and edit manually the secret to be 32 characters long. + +##### Option 2: Automatically (since v5.30.3) + +The `migrateToSecureLegacySignature` property is used to generate a new secret token if the length is 64 characters. + +``` +security: + api: + migrateToSecureLegacySignature: true +``` + +The token will be automatically updated to 32 characters long and the application will start without any issues. +The property won't have any other effect on the application and could be removed after the secret is updated. + ::: -By default verdaccio uses a little database to store private packages the `storage` property is defined in the `config.yaml` file. - -The location might change based in your operative system, see [here](cli.md) more details about location of files. +_The `.verdaccio-db` file database is only available if user does not use a custom storage_, by default verdaccio uses a tiny database to store private packages the `storage` property is defined in the `config.yaml` file. +The location might change based on your operating system. [Read the CLI section](cli.md) for more details about the location of files. The structure of the database is based in JSON file, for instance: ```json { "list": ["package1", "@scope/pkg2"], - "secret": "secret_token" + "secret": "secret_token_32_characters_long" } ``` - `list`: Is an array with the list of the private packages published, any item on this list is considered being published by the user. -- `secret`: The secret field is used for verify the token signature, either for _JWT_ or legacy token signature. +- `secret`: The secret field is used for the token signature and verification, either for _JWT_ or legacy token signature. ### Plugins {#plugins} @@ -86,14 +122,47 @@ auth: ### Security {#security} -The security block allows you to customise the token signature. To enable a new [JWT (JSON Web Tokens)](https://jwt.io/) signature you need to add the block `jwt` to the `api` section; `web` uses `jwt` by default. +The security block permits customization of the token signature with two options. The configuration is divided into +two sections, `api` and `web`. When using JWT on `api`, it must be defined; otherwise, the legacy token signature (`aes192`) will be utilized. -The configuration is separated in two sections, `api` and `web`. To use JWT on `api` it has to be defined, otherwise the legacy token signature (`aes192`) will be used. For JWT you might want to customize the [signature](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) and the token [verification](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) with your own properties. +#### How to the token is generated? +The token signature requires a **secret token** generated by custom plugin that creates the `.verdaccio-db` database or in case a custom storage is used, +the secret token is fetched from the plugin implementation itself. In any case the _secret token_ is required to start the application. + +#### Legacy Token Signature + +The `legacy` property is used to enable the legacy token signature. **By default is enabled**. The legacy feature only applies to the API, the web UI uses JWT by default. + +:::info + +In 5.x versions using Node.js 21 or lower, there will see the warning `[DEP0106] DeprecationWarning: crypto.createDecipher is deprecated`. printed in your terminal. +This warning indicates that Node.js has deprecated a function utilized by the legacy signature. + +If verdaccio runs on **Node.js 22** or higher, you will not see this warning since a new modern legacy signature has been implemented. + +The **migrateToSecureLegacySignature** property is only available for versions higher than 5.30.3 and is **false** by default. + +:::info + +```yaml +security: + api: + legacy: true # by default is true even if this section is not defined + migrateToSecureLegacySignature: true # will generate a new secret token if the length is 64 characters ``` + +#### JWT Token Signature + +To enable a new [JWT (JSON Web Tokens)](https://jwt.io/) signature, the `jwt` block needs to be added to the `api` section; `jwt` is utilized by default in `web`. + +By using the JWT signature is also possible to customize the [signature](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) and the token [verification](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) with your own properties. + +```yaml security: api: legacy: true + migrateToSecureLegacySignature: true # will generate a new secret token if the length is 64 characters jwt: sign: expiresIn: 29d @@ -106,17 +175,11 @@ security: someProp: [value] ``` -:::info - -In 5.x versions, you will see the warning `[DEP0106] DeprecationWarning: crypto.createDecipher is deprecated`. in your terminal. This warning indicates that Node.js has deprecated a function utilized by the legacy signature. **To address thi please upgrade to the newest 6.x version (to check the release status read here [available](https://github.com/verdaccio/verdaccio/discussions/4018)).** - -:::info - ### Server {#server} A set of properties to modify the behavior of the server application, specifically the API (Express.js). -> You can specify HTTP/1.1 server keep alive timeout in seconds for incomming connections. +> You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections. > A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout. > WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough. diff --git a/website/versioned_docs/version-6.x/config.md b/website/versioned_docs/version-6.x/config.md index 3111b067d..b55449794 100644 --- a/website/versioned_docs/version-6.x/config.md +++ b/website/versioned_docs/version-6.x/config.md @@ -45,25 +45,28 @@ storage: ./storage ### The `.verdaccio-db` database {#.verdaccio-db} -:::info -Only available if user does not use a custom storage -::: +The tiny database is used to store private packages published by the user. The database is based on a JSON file that contains +the list of private packages published and the secret token used for the token signature. +It is created automatically when starting the application for the first time. -By default verdaccio uses a little database to store private packages the `storage` property is defined in the `config.yaml` file. +The location of the database is based on the `config.yaml` folder location, for instance: -The location might change based in your operative system, see [here](cli.md) more details about location of files. +If the `config.yaml` is located in `/some_local_path/config.yaml`, the database will be created in `/some_local_path/storage/.verdaccio-db`. + +_The `.verdaccio-db` file database is only available if user does not use a custom storage_, by default verdaccio uses a tiny database to store private packages the `storage` property is defined in the `config.yaml` file. +The location might change based on your operating system. [Read the CLI section](cli.md) for more details about the location of files. The structure of the database is based in JSON file, for instance: ```json { "list": ["package1", "@scope/pkg2"], - "secret": "secret_token" + "secret": "secret_token_32_characters_long" } ``` - `list`: Is an array with the list of the private packages published, any item on this list is considered being published by the user. -- `secret`: The secret field is used for verify the token signature, either for _JWT_ or legacy token signature. +- `secret`: The secret field is used for the token signature and verification, either for _JWT_ or legacy token signature. ### Plugins {#plugins} @@ -86,48 +89,50 @@ auth: ### Token signature {#token} -The default token signature is based on the Advanced Encryption Standard (AES) with the algorithm `aes192`, known as _legacy_. It's important to note that legacy tokens are not designed to expire. If expiration functionality is needed, it is recommended to use _JSON Web Tokens (JWT)_ instead. +The default token signature is based on the Advanced Encryption Standard (AES) with the algorithm `aes-256-ctr`, known as _legacy_. +It's important to note that legacy tokens are not designed to expire. +If expiration functionality is needed, it is recommended to use _JSON Web Tokens (JWT)_ instead. #### Security {#security} -The security block allows you to customise the token signature. To enable a new [JWT (JSON Web Tokens)](https://jwt.io/) signature you need to add the block `jwt` to the `api` section; `web` uses `jwt` by default. +The security block permits customization of the token signature with two options. The configuration is divided into +two sections, `api` and `web`. When using JWT on `api`, it must be defined; otherwise, the legacy token signature (`aes192`) will be utilized. -The configuration is separated in two sections, `api` and `web`. To use JWT on `api` it has to be defined, otherwise the legacy token signature (`aes192`) will be used. For JWT you might want to customize the [signature](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) and the token [verification](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) with your own properties. +#### How to the token is generated? -``` +The token signature requires a **secret token** generated by custom plugin that creates the `.verdaccio-db` database or in case a custom storage is used, +the secret token is fetched from the plugin implementation itself. In any case the _secret token_ is required to start the application. + +#### Legacy Token Signature + +The `legacy` property is used to enable the legacy token signature. **By default is enabled**. The legacy feature only applies to the API, the web UI uses JWT by default. + +:::info + +The **migrateToSecureLegacySignature** property is **true** by default on 6.x versions or higher and could be disabled but is not recommended otherwise will cause issues using Node.js 22 or higher. + +:::info + +```yaml +# This configuration is the default one only displayed for reference, +# no need to define it in your config file. +security: + api: + legacy: true # by default is true even if this section is not defined + migrateToSecureLegacySignature: true # will generate a new secret token if the length is 64 characters +``` + +#### JWT Token Signature + +To enable a new [JWT (JSON Web Tokens)](https://jwt.io/) signature, the `jwt` block needs to be added to the `api` section; `jwt` is utilized by default in `web`. + +By using the JWT signature is also possible to customize the [signature](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) and the token [verification](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) with your own properties. + +```yaml security: - enhancedLegacySignature: false - api: - legacy: true - jwt: - sign: - expiresIn: 29d - verify: - someProp: [value] - web: - sign: - expiresIn: 1h # 1 hour by default - verify: - someProp: [value] -``` - -#### `enhancedLegacySignature` {#enhancedLegacySignature} - -In certain instances, particularly in older installations, you might encounter the warning `[DEP0106] DeprecationWarning: crypto.createDecipher is deprecated`. in your terminal. This warning indicates that Node.js has deprecated a function utilized by the legacy signature. To address this, you can enable the `enhancedLegacySignature` property, which switches the legacy token signature to one based on AES-192 with an initialization vector. - -:::caution - -It is crucial to emphasize that enabling this option will lead to the invalidation of previous tokens. - -For all 6.x versions, the property `enhancedLegacySignature` is set to `false` by default upon initialization, to change that behaviour follow as illustrated in the following example. - -:::caution - -``` -security: - enhancedLegacySignature: true api: legacy: true + migrateToSecureLegacySignature: true # will generate a new secret token if the length is 64 characters jwt: sign: expiresIn: 29d