0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-30 22:34:10 -05:00

feat: signature package (#3653)

* feat: signature package

* feat: signature package
This commit is contained in:
Juan Picado 2023-02-26 13:19:22 +01:00 committed by GitHub
parent 399cf9c47c
commit ddb6a22396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 298 additions and 32 deletions

View file

@ -0,0 +1,8 @@
---
'@verdaccio/auth': minor
'@verdaccio/config': minor
'@verdaccio/signature': minor
'@verdaccio/ui-components': minor
---
feat: signature package

View file

@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, {
coverageThreshold: { coverageThreshold: {
global: { global: {
// FIXME: increase to 90 // FIXME: increase to 90
lines: 42, lines: 30,
}, },
}, },
}); });

View file

@ -43,10 +43,10 @@
"@verdaccio/config": "workspace:6.0.0-6-next.62", "@verdaccio/config": "workspace:6.0.0-6-next.62",
"@verdaccio/loaders": "workspace:6.0.0-6-next.31", "@verdaccio/loaders": "workspace:6.0.0-6-next.31",
"@verdaccio/logger": "workspace:6.0.0-6-next.30", "@verdaccio/logger": "workspace:6.0.0-6-next.30",
"@verdaccio/signature": "workspace:6.0.0-6-next.1",
"@verdaccio/utils": "workspace:6.0.0-6-next.30", "@verdaccio/utils": "workspace:6.0.0-6-next.30",
"debug": "4.3.4", "debug": "4.3.4",
"express": "4.18.2", "express": "4.18.2",
"jsonwebtoken": "9.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"verdaccio-htpasswd": "workspace:11.0.0-6-next.32" "verdaccio-htpasswd": "workspace:11.0.0-6-next.32"
}, },

View file

@ -15,6 +15,7 @@ import {
} from '@verdaccio/core'; } from '@verdaccio/core';
import { asyncLoadPlugin } from '@verdaccio/loaders'; import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger'; import { logger } from '@verdaccio/logger';
import { aesEncrypt, parseBasicPayload, signPayload } from '@verdaccio/signature';
import { import {
AllowAccess, AllowAccess,
Callback, Callback,
@ -27,9 +28,6 @@ import {
} from '@verdaccio/types'; } from '@verdaccio/types';
import { getMatchedPackagesSpec, isFunction, isNil } from '@verdaccio/utils'; import { getMatchedPackagesSpec, isFunction, isNil } from '@verdaccio/utils';
import { signPayload } from './jwt-token';
import { aesEncrypt } from './legacy-token';
import { parseBasicPayload } from './token';
import { import {
convertPayloadToBase64, convertPayloadToBase64,
getDefaultPlugins, getDefaultPlugins,
@ -47,6 +45,7 @@ export interface TokenEncryption {
aesEncrypt(buf: string): string | void; aesEncrypt(buf: string): string | void;
} }
// remove
export interface AESPayload { export interface AESPayload {
user: string; user: string;
password: string; password: string;

View file

@ -1,5 +1,2 @@
export { Auth, TokenEncryption } from './auth'; export { Auth } from './auth';
export * from './utils'; export * from './utils';
export * from './legacy-token';
export * from './jwt-token';
export * from './token';

View file

@ -11,12 +11,10 @@ import {
errorUtils, errorUtils,
pluginUtils, pluginUtils,
} from '@verdaccio/core'; } from '@verdaccio/core';
import { aesDecrypt, parseBasicPayload, verifyPayload } from '@verdaccio/signature';
import { AuthPackageAllow, Config, Logger, RemoteUser, Security } from '@verdaccio/types'; import { AuthPackageAllow, Config, Logger, RemoteUser, Security } from '@verdaccio/types';
import { AESPayload, TokenEncryption } from './auth'; import { AESPayload, TokenEncryption } from './auth';
import { verifyPayload } from './jwt-token';
import { aesDecrypt } from './legacy-token';
import { parseBasicPayload } from './token';
const debug = buildDebug('verdaccio:auth:utils'); const debug = buildDebug('verdaccio:auth:utils');

View file

@ -17,24 +17,22 @@ import {
errorUtils, errorUtils,
} from '@verdaccio/core'; } from '@verdaccio/core';
import { setup } from '@verdaccio/logger'; import { setup } from '@verdaccio/logger';
import { aesDecrypt, signPayload, verifyPayload } from '@verdaccio/signature';
import { Config, RemoteUser, Security } from '@verdaccio/types'; import { Config, RemoteUser, Security } from '@verdaccio/types';
import { buildToken, buildUserBuffer, getAuthenticatedMessage } from '@verdaccio/utils'; import { buildToken, buildUserBuffer, getAuthenticatedMessage } from '@verdaccio/utils';
import type { AllowActionCallbackResponse } from '@verdaccio/utils';
import { import {
ActionsAllowed, ActionsAllowed,
AllowActionCallbackResponse,
Auth, Auth,
aesDecrypt,
allow_action, allow_action,
getApiToken, getApiToken,
getDefaultPlugins, getDefaultPlugins,
getMiddlewareCredentials, getMiddlewareCredentials,
signPayload,
verifyJWTPayload, verifyJWTPayload,
verifyPayload,
} from '../src'; } from '../src';
setup([]); setup({});
const parseConfigurationFile = (conf) => { const parseConfigurationFile = (conf) => {
const { name, ext } = path.parse(conf); const { name, ext } = path.parse(conf);
@ -452,6 +450,7 @@ describe('Auth utilities', () => {
const config: Config = getConfig('security-legacy', secret); const config: Config = getConfig('security-legacy', secret);
const auth: Auth = new Auth(config); const auth: Auth = new Auth(config);
await auth.init(); await auth.init();
// @ts-expect-error
const token = auth.aesEncrypt(null); const token = auth.aesEncrypt(null);
const security: Security = config.security; const security: Security = config.security;
const credentials = getMiddlewareCredentials( const credentials = getMiddlewareCredentials(

View file

@ -19,6 +19,9 @@
{ {
"path": "../loaders" "path": "../loaders"
}, },
{
"path": "../signature"
},
{ {
"path": "../logger/logger" "path": "../logger/logger"
}, },

View file

@ -4,6 +4,7 @@ export const TOKEN_VALID_LENGTH = 32;
/** /**
* Secret key must have 32 characters. * Secret key must have 32 characters.
* @deprecated
*/ */
export function generateRandomSecretKey(): string { export function generateRandomSecretKey(): string {
return randomBytes(TOKEN_VALID_LENGTH).toString('base64').substring(0, TOKEN_VALID_LENGTH); return randomBytes(TOKEN_VALID_LENGTH).toString('base64').substring(0, TOKEN_VALID_LENGTH);

View file

@ -0,0 +1,3 @@
{
"extends": "../../.babelrc"
}

View file

@ -0,0 +1,3 @@
const config = require('../../jest/config');
module.exports = Object.assign({}, config, {});

View file

@ -0,0 +1,53 @@
{
"name": "@verdaccio/signature",
"version": "6.0.0-6-next.1",
"description": "verdaccio signature utils",
"main": "./build/index.js",
"types": "build/index.d.ts",
"author": {
"name": "Juan Picado",
"email": "juanpicado19@gmail.com"
},
"repository": {
"type": "https",
"url": "https://github.com/verdaccio/verdaccio"
},
"license": "MIT",
"homepage": "https://verdaccio.org",
"keywords": [
"private",
"package",
"repository",
"registry",
"enterprise",
"modules",
"proxy",
"server",
"verdaccio"
],
"engines": {
"node": ">=12"
},
"scripts": {
"clean": "rimraf ./build",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"watch": "pnpm build:js -- --watch",
"build": "pnpm run build:js && pnpm run build:types"
},
"dependencies": {
"jsonwebtoken": "9.0.0",
"debug": "4.3.4",
"lodash": "4.17.21"
},
"devDependencies": {
"@verdaccio/config": "workspace:6.0.0-6-next.62",
"@verdaccio/types": "workspace:11.0.0-6-next.21"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/verdaccio"
}
}

View file

@ -0,0 +1,10 @@
export {
aesDecryptDeprecated,
aesEncryptDeprecated,
generateRandomSecretKeyDeprecated,
} from './legacy-signature';
export { aesDecrypt, aesEncrypt } from './signature';
export { signPayload, verifyPayload } from './jwt-token';
export * as utils from './utils';
export * as types from './types';
export { parseBasicPayload } from './token';

View file

@ -0,0 +1,51 @@
import { createCipher, createDecipher } from 'crypto';
import { generateRandomHexString } from '../utils';
export const defaultAlgorithm = 'aes192';
export const defaultTarballHashAlgorithm = 'sha1';
/**
*
* @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]);
}
/**
*
* @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;
/**
* Genrate a secret key of 64 characters.
* @deprecated keys should be length max of 64
*/
export function generateRandomSecretKeyDeprecated(): string {
return generateRandomHexString(6);
}

View file

@ -14,8 +14,6 @@ const debug = buildDebug('verdaccio:auth:token:legacy');
export const defaultAlgorithm = process.env.VERDACCIO_LEGACY_ALGORITHM || 'aes-256-ctr'; export const defaultAlgorithm = process.env.VERDACCIO_LEGACY_ALGORITHM || 'aes-256-ctr';
const inputEncoding: CharacterEncoding = 'utf8'; const inputEncoding: CharacterEncoding = 'utf8';
const outputEncoding: BinaryToTextEncoding = 'hex'; const outputEncoding: BinaryToTextEncoding = 'hex';
// For AES, this is always 16
const IV_LENGTH = 16;
// Must be 256 bits (32 characters) // Must be 256 bits (32 characters)
// https://stackoverflow.com/questions/50963160/invalid-key-length-in-crypto-createcipheriv#50963356 // https://stackoverflow.com/questions/50963160/invalid-key-length-in-crypto-createcipheriv#50963356
const VERDACCIO_LEGACY_ENCRYPTION_KEY = process.env.VERDACCIO_LEGACY_ENCRYPTION_KEY; const VERDACCIO_LEGACY_ENCRYPTION_KEY = process.env.VERDACCIO_LEGACY_ENCRYPTION_KEY;
@ -25,7 +23,8 @@ export function aesEncrypt(value: string, key: string): string | void {
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/ // https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
debug('encrypt %o', value); debug('encrypt %o', value);
debug('algorithm %o', defaultAlgorithm); debug('algorithm %o', defaultAlgorithm);
const iv = Buffer.from(randomBytes(IV_LENGTH)); // IV must be a buffer of length 16
const iv = Buffer.from(randomBytes(16));
const secretKey = VERDACCIO_LEGACY_ENCRYPTION_KEY || key; const secretKey = VERDACCIO_LEGACY_ENCRYPTION_KEY || key;
const isKeyValid = secretKey?.length === TOKEN_VALID_LENGTH; const isKeyValid = secretKey?.length === TOKEN_VALID_LENGTH;
debug('length secret key %o', secretKey?.length); debug('length secret key %o', secretKey?.length);

View file

@ -1,5 +1,10 @@
import { BasicPayload } from './utils'; import { BasicPayload } from './types';
/**
*
* @param credentials
* @returns
*/
export function parseBasicPayload(credentials: string): BasicPayload { export function parseBasicPayload(credentials: string): BasicPayload {
const index = credentials.indexOf(':'); const index = credentials.indexOf(':');
if (index < 0) { if (index < 0) {

View file

@ -0,0 +1,6 @@
export interface AESPayload {
user: string;
password: string;
}
export type BasicPayload = AESPayload | void;

View file

@ -0,0 +1,40 @@
import { Hash, createHash, pseudoRandomBytes, randomBytes } from 'crypto';
export const defaultTarballHashAlgorithm = 'sha1';
/**
*
* @returns
*/
export function createTarballHash(algorithm = defaultTarballHashAlgorithm): Hash {
return createHash(algorithm);
}
/**
* Express doesn't do ETAGS with requests <= 1024b
* we use md5 here, it works well on 1k+ bytes, but with fewer data
* could improve performance using crc32 after benchmarks.
* @param {Object} data
* @return {String}
*/
export function stringToMD5(data: Buffer | string): string {
return createHash('md5').update(data).digest('hex');
}
/**
*
* @param length
* @returns
*/
export function generateRandomHexString(length = 8): string {
return pseudoRandomBytes(length).toString('hex');
}
export const TOKEN_VALID_LENGTH = 32;
/**
* Generate a secret of 32 characters.
*/
export function generateRandomSecretKey(): string {
return randomBytes(TOKEN_VALID_LENGTH).toString('base64').substring(0, TOKEN_VALID_LENGTH);
}

View file

@ -0,0 +1,13 @@
import { createRemoteUser } from '@verdaccio/config';
import { signPayload, verifyPayload } from '../src';
describe('verifyJWTPayload', () => {
test('should verify the token and return a remote user', async () => {
const remoteUser = createRemoteUser('foo', []);
const token = await signPayload(remoteUser, '12345');
const verifiedToken = verifyPayload(token, '12345');
expect(verifiedToken.groups).toEqual(remoteUser.groups);
expect(verifiedToken.name).toEqual(remoteUser.name);
});
});

View file

@ -0,0 +1,23 @@
import {
aesDecryptDeprecated,
aesEncryptDeprecated,
generateRandomSecretKeyDeprecated,
} from '../src';
describe('test deprecated crypto utils', () => {
test('decrypt payload flow', () => {
const secret = generateRandomSecretKeyDeprecated();
const payload = 'juan:password';
const token = aesEncryptDeprecated(Buffer.from(payload), secret);
const data = aesDecryptDeprecated(token, secret);
expect(data.toString()).toEqual(payload.toString());
});
test('crypt fails if secret is incorrect', () => {
const payload = 'juan:password';
expect(aesEncryptDeprecated(Buffer.from(payload), 'fake_token').toString()).not.toEqual(
Buffer.from(payload)
);
});
});

View file

@ -1,4 +1,4 @@
import { aesDecrypt, aesEncrypt } from '../src/legacy-token'; import { aesDecrypt, aesEncrypt } from '../src';
describe('test crypto utils', () => { describe('test crypto utils', () => {
test('decrypt payload flow', () => { test('decrypt payload flow', () => {

View file

@ -0,0 +1,18 @@
import {
TOKEN_VALID_LENGTH,
createTarballHash,
generateRandomSecretKey,
stringToMD5,
} from '../src/utils';
test('token generation length is valid', () => {
expect(generateRandomSecretKey()).toHaveLength(TOKEN_VALID_LENGTH);
});
test('string to md5 has valid length', () => {
expect(stringToMD5(Buffer.from('foo'))).toHaveLength(32);
});
test('create a hash of content', () => {
expect(typeof createTarballHash().update('1').digest('hex')).toEqual('string');
});

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"]
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.reference.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build",
"noImplicitAny": false
},
"include": ["src/**/*.ts", "types/*.d.ts"],
"references": [
{
"path": "../config"
},
{
"path": "../utils"
}
]
}

View file

@ -7,10 +7,6 @@
"homepage": "https://verdaccio.org", "homepage": "https://verdaccio.org",
"main": "./build/index.js", "main": "./build/index.js",
"types": "build/index.d.ts", "types": "build/index.d.ts",
"files": [
"./build",
"./src"
],
"scripts": { "scripts": {
"test": "cross-env TZ=UTC jest --config jest/jest.config.js", "test": "cross-env TZ=UTC jest --config jest/jest.config.js",
"test:html": "cross-env TZ=UTC jest --config jest/jest.config.js --coverage-reporters=html", "test:html": "cross-env TZ=UTC jest --config jest/jest.config.js --coverage-reporters=html",

View file

@ -391,11 +391,11 @@ importers:
'@verdaccio/core': workspace:6.0.0-6-next.62 '@verdaccio/core': workspace:6.0.0-6-next.62
'@verdaccio/loaders': workspace:6.0.0-6-next.31 '@verdaccio/loaders': workspace:6.0.0-6-next.31
'@verdaccio/logger': workspace:6.0.0-6-next.30 '@verdaccio/logger': workspace:6.0.0-6-next.30
'@verdaccio/signature': workspace:6.0.0-6-next.1
'@verdaccio/types': workspace:11.0.0-6-next.21 '@verdaccio/types': workspace:11.0.0-6-next.21
'@verdaccio/utils': workspace:6.0.0-6-next.30 '@verdaccio/utils': workspace:6.0.0-6-next.30
debug: 4.3.4 debug: 4.3.4
express: 4.18.2 express: 4.18.2
jsonwebtoken: 9.0.0
lodash: 4.17.21 lodash: 4.17.21
verdaccio-htpasswd: workspace:11.0.0-6-next.32 verdaccio-htpasswd: workspace:11.0.0-6-next.32
dependencies: dependencies:
@ -403,10 +403,10 @@ importers:
'@verdaccio/core': link:../core/core '@verdaccio/core': link:../core/core
'@verdaccio/loaders': link:../loaders '@verdaccio/loaders': link:../loaders
'@verdaccio/logger': link:../logger/logger '@verdaccio/logger': link:../logger/logger
'@verdaccio/signature': link:../signature
'@verdaccio/utils': link:../utils '@verdaccio/utils': link:../utils
debug: 4.3.4 debug: 4.3.4
express: 4.18.2 express: 4.18.2
jsonwebtoken: 9.0.0
lodash: 4.17.21 lodash: 4.17.21
verdaccio-htpasswd: link:../plugins/htpasswd verdaccio-htpasswd: link:../plugins/htpasswd
devDependencies: devDependencies:
@ -1108,6 +1108,21 @@ importers:
'@verdaccio/types': link:../../core/types '@verdaccio/types': link:../../core/types
ts-node: 10.9.1_5cc4fd05cdbba7efed0227e61dc065e1 ts-node: 10.9.1_5cc4fd05cdbba7efed0227e61dc065e1
packages/signature:
specifiers:
'@verdaccio/config': workspace:6.0.0-6-next.62
'@verdaccio/types': workspace:11.0.0-6-next.21
debug: 4.3.4
jsonwebtoken: 9.0.0
lodash: 4.17.21
dependencies:
debug: 4.3.4
jsonwebtoken: 9.0.0
lodash: 4.17.21
devDependencies:
'@verdaccio/config': link:../config
'@verdaccio/types': link:../core/types
packages/standalone: packages/standalone:
specifiers: specifiers:
'@verdaccio/cli': workspace:6.0.0-6-next.62 '@verdaccio/cli': workspace:6.0.0-6-next.62
@ -10911,7 +10926,7 @@ packages:
dev: true dev: true
/buffer-equal-constant-time/1.0.1: /buffer-equal-constant-time/1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=}
/buffer-from/1.1.1: /buffer-from/1.1.1:
resolution: {integrity: sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==} resolution: {integrity: sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==}
@ -15583,7 +15598,7 @@ packages:
map-cache: 0.2.2 map-cache: 0.2.2
/fresh/0.5.2: /fresh/0.5.2:
resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=} resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
/friendly-errors-webpack-plugin/1.7.0_webpack@5.75.0: /friendly-errors-webpack-plugin/1.7.0_webpack@5.75.0:
@ -18164,7 +18179,7 @@ packages:
dev: true dev: true
/jsonparse/1.3.1: /jsonparse/1.3.1:
resolution: {integrity: sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=} resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0} engines: {'0': node >= 0.2.0}
dev: false dev: false
@ -19074,7 +19089,7 @@ packages:
dev: true dev: true
/merge-descriptors/1.0.1: /merge-descriptors/1.0.1:
resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=} resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
/merge-stream/2.0.0: /merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}