diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 9af07cd6..744ee2fc 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18, 20, 21] + node-version: [18, 20, 22] os: [ubuntu-latest] steps: @@ -83,7 +83,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16, 18, 20] + node-version: [18, 20, 22] os: [ubuntu-latest] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8ef203c..2847e460 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,5 +10,3 @@ jobs: secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} - with: - checkTest: false diff --git a/app/common/CryptoUtil.ts b/app/common/CryptoUtil.ts index 478c5c59..4e9e7ceb 100644 --- a/app/common/CryptoUtil.ts +++ b/app/common/CryptoUtil.ts @@ -1,4 +1,5 @@ -import { generateKeyPairSync, publicEncrypt, privateDecrypt, constants } from 'crypto'; +import { generateKeyPairSync } from 'crypto'; +import NodeRSA from 'node-rsa'; // generate rsa key pair export function genRSAKeys(): { publicKey: string, privateKey: string } { @@ -17,17 +18,19 @@ export function genRSAKeys(): { publicKey: string, privateKey: string } { } // encrypt rsa private key -export function encryptRSA(publicKey: string, data: string): string { - return publicEncrypt({ - key: publicKey, - padding: constants.RSA_PKCS1_PADDING, - }, Buffer.from(data, 'utf8')).toString('base64'); +export function encryptRSA(publicKey: string, plainText: string): string { + const key = new NodeRSA(publicKey, 'pkcs1-public-pem', { + encryptionScheme: 'pkcs1', + environment: 'browser', + }); + return key.encrypt(plainText, 'base64'); } // decrypt rsa private key -export function decryptRSA(privateKey: string, data: string) { - return privateDecrypt({ - key: privateKey, - padding: constants.RSA_PKCS1_PADDING, - }, Buffer.from(data, 'base64')).toString('utf8'); +export function decryptRSA(privateKey: string, encryptedBase64: string): string { + const key = new NodeRSA(privateKey, 'pkcs1-private-pem', { + encryptionScheme: 'pkcs1', + environment: 'browser', + }); + return key.decrypt(encryptedBase64, 'utf8'); } diff --git a/package.json b/package.json index e8e6665e..6bfea69b 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,6 @@ "url": "git@github.com:cnpm/cnpmcore.git" }, "egg": { - "revert": [ - "CVE-2023-46809" - ], "typescript": true }, "keywords": [ @@ -105,6 +102,7 @@ "lodash": "^4.17.21", "mime-types": "^2.1.35", "mysql2": "^3.9.4", + "node-rsa": "^1.1.1", "npm-package-arg": "^10.1.0", "oss-cnpm": "^5.0.1", "p-map": "^4.0.0", @@ -126,6 +124,7 @@ "@types/mime-types": "^2.1.1", "@types/mocha": "^10.0.1", "@types/mysql": "^2.15.21", + "@types/node-rsa": "^1.1.4", "@types/npm-package-arg": "^6.1.1", "@types/semver": "^7.3.12", "@types/tar": "^6.1.4", diff --git a/test/common/CryptoUtil.test.ts b/test/common/CryptoUtil.test.ts new file mode 100644 index 00000000..ba5fd076 --- /dev/null +++ b/test/common/CryptoUtil.test.ts @@ -0,0 +1,22 @@ +import { strict as assert } from 'node:assert'; +import { genRSAKeys, encryptRSA, decryptRSA } from '../../app/common/CryptoUtil'; + +describe('test/common/CryptoUtil.test.ts', () => { + describe('genRSAKeys()', () => { + it('should work', () => { + const keys = genRSAKeys(); + assert(keys.publicKey); + assert(keys.privateKey); + }); + }); + + describe('encryptRSA(), decryptRSA()', () => { + it('should work', () => { + const keys = genRSAKeys(); + // const plainText = 'hello world 中文😄'; + const plainText = 'hello world 中文'; + const encryptText = encryptRSA(keys.publicKey, plainText); + assert.equal(decryptRSA(keys.privateKey, encryptText), plainText); + }); + }); +}); diff --git a/test/port/webauth/webauthController.test.ts b/test/port/webauth/webauthController.test.ts index 61724e3b..aef0720c 100644 --- a/test/port/webauth/webauthController.test.ts +++ b/test/port/webauth/webauthController.test.ts @@ -1,6 +1,6 @@ -import assert from 'assert'; -import crypto from 'crypto'; -import { basename } from 'path'; +import { strict as assert } from 'node:assert'; +import crypto from 'node:crypto'; +import { basename } from 'node:path'; import { app, mock } from 'egg-mock/bootstrap'; import { AuthAdapter } from '../../../app/infra/AuthAdapter'; import { CacheAdapter } from '../../../app/common/adapter/CacheAdapter'; @@ -32,13 +32,10 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 422); assert.equal(res.body.error, "[INVALID_PARAM] must have required property 'hostname'"); - }); - }); describe('GET /-/v1/login/request/session/:sessionId', () => { - let sessionId = ''; const rsaKeys = genRSAKeys(); beforeEach(async () => { @@ -48,14 +45,12 @@ describe('test/port/webauth/webauthController.test.ts', () => { await cacheAdapter.set(`${sessionId}_privateKey`, rsaKeys.privateKey); }); - it('should check sessionId type', async () => { const res = await app.httpRequest() .get('/-/v1/login/request/session/123'); assert.equal(res.status, 422); assert.equal(res.body.error, '[INVALID_PARAM] sessionId: must NOT have fewer than 36 characters'); - }); it('should check sessionId exists', async () => { @@ -65,7 +60,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 404); assert(/Session not found/.test(res.text)); assert.equal(res.headers['content-type'], 'text/html; charset=utf-8'); - }); it('should render login.html', async () => { @@ -74,13 +68,10 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 200); assert(/Sign in to CNPM<\/title>/.test(res.text)); - }); - }); describe('POST /-/v1/login/request/session/:sessionId', () => { - let sessionId = ''; const rsaKeys = genRSAKeys(); beforeEach(async () => { @@ -90,14 +81,12 @@ describe('test/port/webauth/webauthController.test.ts', () => { await cacheAdapter.set(`${sessionId}_privateKey`, rsaKeys.privateKey); }); - it('should check sessionId type', async () => { const res = await app.httpRequest() .post('/-/v1/login/request/session/123'); assert.equal(res.status, 422); assert.equal(res.body.error, '[INVALID_PARAM] sessionId: must NOT have fewer than 36 characters'); - }); it('should check sessionId exists', async () => { @@ -107,7 +96,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 200); assert(/Session not found/.test(res.text)); assert.equal(res.headers['content-type'], 'application/json; charset=utf-8'); - }); describe('should verify login request body', () => { @@ -122,7 +110,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { }); it('should login success', async () => { - const password = encryptRSA(rsaKeys.publicKey, 'flymetothemoon'); const res = await app.httpRequest() .post(`/-/v1/login/request/session/${sessionId}`) @@ -138,7 +125,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { }); it('should check password', async () => { - const password = encryptRSA(rsaKeys.publicKey, 'incorrect_password'); const res = await app.httpRequest() .post(`/-/v1/login/request/session/${sessionId}`) @@ -151,11 +137,9 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 200); assert(/Please check your login name and password/.test(res.body.message)); - }); it('should check user params', async () => { - const password = encryptRSA(rsaKeys.publicKey, 'incorrect_password'); const res = await app.httpRequest() .post(`/-/v1/login/request/session/${sessionId}`) @@ -168,7 +152,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 200); assert(/Unauthorized, Validation Failed/.test(res.body.message)); - }); it('should check authentication user (unbound webauthn) when enableWebauthn', async () => { @@ -222,11 +205,8 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 200); assert(/Public registration is not allowed/.test(res.body.message)); - }); - }); - }); describe('/-/v1/login/request/prepare/:sessionId', () => { @@ -256,7 +236,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 422); assert.equal(res.body.error, '[INVALID_PARAM] sessionId: must NOT have fewer than 36 characters'); - }); it('should check sessionId exists', async () => { @@ -266,11 +245,9 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 200); assert(/Session not found/.test(res.text)); assert.equal(res.headers['content-type'], 'application/json; charset=utf-8'); - }); it('should get prepare with authentication options', async () => { - const res = await app.httpRequest() .get(`/-/v1/login/request/prepare/${sessionId}?name=banana`); @@ -279,7 +256,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { }); it('should get prepare with registration options', async () => { - const res = await app.httpRequest() .get(`/-/v1/login/request/prepare/${sessionId}?name=apple`); @@ -289,7 +265,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { }); describe('/-/v1/login/sso/:sessionId', () => { - let sessionId = ''; beforeEach(async () => { sessionId = crypto.randomUUID(); @@ -304,7 +279,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { }); it('should sso login work', async () => { - const res = await app.httpRequest() .post(`/-/v1/login/sso/${sessionId}`); @@ -312,7 +286,6 @@ describe('test/port/webauth/webauthController.test.ts', () => { }); it('should check sessionId exists', async () => { - const res = await app.httpRequest() .post('/-/v1/login/sso/banana'); @@ -340,25 +313,20 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 403); assert.equal(res.body.error, '[FORBIDDEN] invalid user info'); }); - }); describe('/-/v1/login/request/success', () => { - it('should work', async () => { - const res = await app.httpRequest() .get('/-/v1/login/request/success'); assert.equal(res.status, 200); assert.equal(res.headers['content-type'], 'text/html; charset=utf-8'); assert(/Authorization Successful/.test(res.text)); - }); }); describe('/-/v1/login/done/session/:sessionId', () => { - let sessionId = ''; beforeEach(async () => { sessionId = crypto.randomUUID(); @@ -368,38 +336,31 @@ describe('test/port/webauth/webauthController.test.ts', () => { it('should check sessionId type', async () => { - const res = await app.httpRequest() .get('/-/v1/login/done/session/123'); assert.equal(res.status, 422); assert.equal(res.body.error, '[INVALID_PARAM] sessionId: must NOT have fewer than 36 characters'); - }); it('should check sessionId exists', async () => { - const res = await app.httpRequest() .get(`/-/v1/login/done/session/${crypto.randomUUID()}`); assert.equal(res.status, 404); assert.equal(res.body.error, '[NOT_FOUND] session not found'); - }); it('should re-validate sessionId', async () => { - const res = await app.httpRequest() .get(`/-/v1/login/done/session/${sessionId}`); assert.equal(res.status, 202); assert.equal(res.body.message, 'processing'); - assert.equal(res.headers['retry-after'], 1); - + assert.equal(res.headers['retry-after'], '1'); }); it('should check sessionId exists', async () => { - const cacheAdapter = await app.getEggObject(CacheAdapter); await cacheAdapter.set(sessionId, 'banana'); const res = await app.httpRequest() @@ -407,9 +368,7 @@ describe('test/port/webauth/webauthController.test.ts', () => { assert.equal(res.status, 200); assert.equal(res.body.token, 'banana'); - - assert(await cacheAdapter.get(sessionId) === null); - + assert.equal(await cacheAdapter.get(sessionId), null); }); }); });