Skip to content

Commit

Permalink
feat: signature package (#3653)
Browse files Browse the repository at this point in the history
* feat: signature package

* feat: signature package
  • Loading branch information
juanpicado authored Feb 26, 2023
1 parent 399cf9c commit ddb6a22
Show file tree
Hide file tree
Showing 27 changed files with 298 additions and 32 deletions.
8 changes: 8 additions & 0 deletions .changeset/kind-ladybugs-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@verdaccio/auth': minor
'@verdaccio/config': minor
'@verdaccio/signature': minor
'@verdaccio/ui-components': minor
---

feat: signature package
2 changes: 1 addition & 1 deletion packages/auth/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 42,
lines: 30,
},
},
});
2 changes: 1 addition & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
"@verdaccio/config": "workspace:6.0.0-6-next.62",
"@verdaccio/loaders": "workspace:6.0.0-6-next.31",
"@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",
"debug": "4.3.4",
"express": "4.18.2",
"jsonwebtoken": "9.0.0",
"lodash": "4.17.21",
"verdaccio-htpasswd": "workspace:11.0.0-6-next.32"
},
Expand Down
5 changes: 2 additions & 3 deletions packages/auth/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@verdaccio/core';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger';
import { aesEncrypt, parseBasicPayload, signPayload } from '@verdaccio/signature';
import {
AllowAccess,
Callback,
Expand All @@ -27,9 +28,6 @@ import {
} from '@verdaccio/types';
import { getMatchedPackagesSpec, isFunction, isNil } from '@verdaccio/utils';

import { signPayload } from './jwt-token';
import { aesEncrypt } from './legacy-token';
import { parseBasicPayload } from './token';
import {
convertPayloadToBase64,
getDefaultPlugins,
Expand All @@ -47,6 +45,7 @@ export interface TokenEncryption {
aesEncrypt(buf: string): string | void;
}

// remove
export interface AESPayload {
user: string;
password: string;
Expand Down
5 changes: 1 addition & 4 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export { Auth, TokenEncryption } from './auth';
export { Auth } from './auth';
export * from './utils';
export * from './legacy-token';
export * from './jwt-token';
export * from './token';
4 changes: 1 addition & 3 deletions packages/auth/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import {
errorUtils,
pluginUtils,
} from '@verdaccio/core';
import { aesDecrypt, parseBasicPayload, verifyPayload } from '@verdaccio/signature';
import { AuthPackageAllow, Config, Logger, RemoteUser, Security } from '@verdaccio/types';

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');

Expand Down
9 changes: 4 additions & 5 deletions packages/auth/test/auth-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,22 @@ import {
errorUtils,
} from '@verdaccio/core';
import { setup } from '@verdaccio/logger';
import { aesDecrypt, signPayload, verifyPayload } from '@verdaccio/signature';
import { Config, RemoteUser, Security } from '@verdaccio/types';
import { buildToken, buildUserBuffer, getAuthenticatedMessage } from '@verdaccio/utils';
import type { AllowActionCallbackResponse } from '@verdaccio/utils';

import {
ActionsAllowed,
AllowActionCallbackResponse,
Auth,
aesDecrypt,
allow_action,
getApiToken,
getDefaultPlugins,
getMiddlewareCredentials,
signPayload,
verifyJWTPayload,
verifyPayload,
} from '../src';

setup([]);
setup({});

const parseConfigurationFile = (conf) => {
const { name, ext } = path.parse(conf);
Expand Down Expand Up @@ -452,6 +450,7 @@ describe('Auth utilities', () => {
const config: Config = getConfig('security-legacy', secret);
const auth: Auth = new Auth(config);
await auth.init();
// @ts-expect-error
const token = auth.aesEncrypt(null);
const security: Security = config.security;
const credentials = getMiddlewareCredentials(
Expand Down
3 changes: 3 additions & 0 deletions packages/auth/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
{
"path": "../loaders"
},
{
"path": "../signature"
},
{
"path": "../logger/logger"
},
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const TOKEN_VALID_LENGTH = 32;

/**
* Secret key must have 32 characters.
* @deprecated
*/
export function generateRandomSecretKey(): string {
return randomBytes(TOKEN_VALID_LENGTH).toString('base64').substring(0, TOKEN_VALID_LENGTH);
Expand Down
3 changes: 3 additions & 0 deletions packages/signature/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../.babelrc"
}
3 changes: 3 additions & 0 deletions packages/signature/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const config = require('../../jest/config');

module.exports = Object.assign({}, config, {});
53 changes: 53 additions & 0 deletions packages/signature/package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]"
},
"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"
}
}
10 changes: 10 additions & 0 deletions packages/signature/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
File renamed without changes.
51 changes: 51 additions & 0 deletions packages/signature/src/legacy-signature/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ const debug = buildDebug('verdaccio:auth:token:legacy');
export const defaultAlgorithm = process.env.VERDACCIO_LEGACY_ALGORITHM || 'aes-256-ctr';
const inputEncoding: CharacterEncoding = 'utf8';
const outputEncoding: BinaryToTextEncoding = 'hex';
// For AES, this is always 16
const IV_LENGTH = 16;
// Must be 256 bits (32 characters)
// https://stackoverflow.com/questions/50963160/invalid-key-length-in-crypto-createcipheriv#50963356
const VERDACCIO_LEGACY_ENCRYPTION_KEY = process.env.VERDACCIO_LEGACY_ENCRYPTION_KEY;
Expand All @@ -25,7 +23,8 @@ export function aesEncrypt(value: string, key: string): string | void {
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
debug('encrypt %o', value);
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 isKeyValid = secretKey?.length === TOKEN_VALID_LENGTH;
debug('length secret key %o', secretKey?.length);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { BasicPayload } from './utils';
import { BasicPayload } from './types';

/**
*
* @param credentials
* @returns
*/
export function parseBasicPayload(credentials: string): BasicPayload {
const index = credentials.indexOf(':');
if (index < 0) {
Expand Down
6 changes: 6 additions & 0 deletions packages/signature/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface AESPayload {
user: string;
password: string;
}

export type BasicPayload = AESPayload | void;
40 changes: 40 additions & 0 deletions packages/signature/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
13 changes: 13 additions & 0 deletions packages/signature/test/jwt.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
23 changes: 23 additions & 0 deletions packages/signature/test/legacy-token-deprecated.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { aesDecrypt, aesEncrypt } from '../src/legacy-token';
import { aesDecrypt, aesEncrypt } from '../src';

describe('test crypto utils', () => {
test('decrypt payload flow', () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/signature/test/utilts.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
Loading

0 comments on commit ddb6a22

Please sign in to comment.