From f70e74ae446dd43f1a3a5a2b71a7bb7dc6a0598d Mon Sep 17 00:00:00 2001 From: Alexander Semyenov Date: Fri, 15 Mar 2024 13:17:27 +0000 Subject: [PATCH] . --- .gitignore | 3 +++ .vscode/extensions.json | 4 ++++ .vscode/settings.json | 3 +++ defs/TestUserInfo.json | 32 ++++++++++++++++---------------- keys/.gitkeep | 0 lib/bullmq/index.ts | 4 ++-- lib/guard/t1.ts | 15 ++++++++------- lib/guard/t2.ts | 13 ++++++++----- lib/jose/keys.ts | 24 ++++++++++++++++++++++++ lib/jose/sign.ts | 41 +++++++++++++++++++++++++++++++++++++++++ lib/jose/types.ts | 5 +++++ lib/ws/client.ts | 24 ++++++++++++++---------- lib/ws/server.ts | 31 +++++++++++++++---------------- scripts/keys.ts | 36 ++++++++++++++++++++++++++++++++++++ src/client/proxy.ts | 14 +++++++------- src/workers/test.ts | 36 ++++++++++++++++++++++++++++++++++++ 16 files changed, 222 insertions(+), 63 deletions(-) create mode 100644 keys/.gitkeep create mode 100644 lib/jose/keys.ts create mode 100644 lib/jose/sign.ts create mode 100644 lib/jose/types.ts create mode 100644 scripts/keys.ts create mode 100644 src/workers/test.ts diff --git a/.gitignore b/.gitignore index 531793a..4a4628d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# App +keys/*.jwk + # Nuxt .nuxt .nitro diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 049a736..50d9926 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -19,16 +19,19 @@ "christian-kohler.path-intellisense", "Codeium.codeium", "cpylua.language-postcss", + "cweijan.vscode-redis-client", "dbaeumer.vscode-eslint", "eamodio.gitlens", "editorconfig.editorconfig", "esbenp.prettier-vscode", + "fagnercarvalho.redis-lsp", "formulahendry.auto-rename-tag", "foxundermoon.shell-format", "helixquar.randomeverything", "heybourn.headwind", "howardzuo.vscode-npm-dependency", "IBM.output-colorizer", + "idered.npm", "kisstkondoros.vscode-codemetrics", "lacroixdavid1.vscode-format-context-menu", "lokalise.i18n-ally", @@ -40,6 +43,7 @@ "micnil.vscode-checkpoints", "mikestead.dotenv", "mkxml.vscode-filesize", + "mongodb.mongodb-vscode", "ms-vscode.vscode-js-profile-flame", "Nuxt.mdc", "oderwat.indent-rainbow", diff --git a/.vscode/settings.json b/.vscode/settings.json index dd31e0e..5d1cf3f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -71,5 +71,8 @@ }, "[shellscript]": { "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" } } diff --git a/defs/TestUserInfo.json b/defs/TestUserInfo.json index 2458a67..76b6d47 100644 --- a/defs/TestUserInfo.json +++ b/defs/TestUserInfo.json @@ -1,18 +1,18 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "TestUserInfo", - "title": "Test User Info", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string" - } + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "TestUserInfo", + "title": "Test User Info", + "type": "object", + "properties": { + "name": { + "type": "string" }, - "required": [ - "name", - "email" - ] -} \ No newline at end of file + "email": { + "type": "string" + } + }, + "required": [ + "name", + "email" + ] +} diff --git a/keys/.gitkeep b/keys/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/bullmq/index.ts b/lib/bullmq/index.ts index d52f970..be15f54 100644 --- a/lib/bullmq/index.ts +++ b/lib/bullmq/index.ts @@ -1,8 +1,8 @@ import { Queue } from 'bullmq' export const bullmq = new Queue< -{ message: string }, -{ status: number } + { message: string }, + { status: number } >('appQueue', { connection: { diff --git a/lib/guard/t1.ts b/lib/guard/t1.ts index ddd6634..c0257e4 100644 --- a/lib/guard/t1.ts +++ b/lib/guard/t1.ts @@ -1,3 +1,4 @@ +import consola from 'consola' import * as jose from 'jose' const alg = 'ES256' @@ -5,10 +6,10 @@ const alg = 'ES256' const keys = await jose.generateKeyPair(alg, { extractable: true }) const keys2 = await jose.generateKeyPair(alg, { extractable: true }) -console.log('keys\n', await jose.exportPKCS8(keys.privateKey)) -console.log('keys2\n', await jose.exportPKCS8(keys2.privateKey)) +consola.log('keys\n', await jose.exportPKCS8(keys.privateKey)) +consola.log('keys2\n', await jose.exportPKCS8(keys2.privateKey)) -const jwt = await new jose.GeneralSign( +const jws = await new jose.GeneralSign( new TextEncoder().encode('Hello, World!'), ) .addSignature(keys.privateKey) @@ -17,12 +18,12 @@ const jwt = await new jose.GeneralSign( .setProtectedHeader({ alg, b64: true }) .sign() -console.log('jwt', jwt) +consola.log('jws', jws) const { payload, protectedHeader } = await jose.generalVerify( - jwt, + jws, keys2.publicKey, ) -console.log(protectedHeader) -console.log(new TextDecoder().decode(payload)) +consola.log(protectedHeader) +consola.log(new TextDecoder().decode(payload)) diff --git a/lib/guard/t2.ts b/lib/guard/t2.ts index 906e3d1..0ca5372 100644 --- a/lib/guard/t2.ts +++ b/lib/guard/t2.ts @@ -1,3 +1,4 @@ +import consola from 'consola' import * as jose from 'jose' const alg = 'ES256' @@ -10,6 +11,8 @@ const keys = await jose.generateKeyPair(alg, { extractable: true }) const keys2 = await jose.generateKeyPair(alg, { extractable: true }) // const keys3 = await jose.generateKeyPair(alg, { extractable: true }); +consola.info(keys.publicKey) + const jwk1 = await jose.exportJWK(keys.publicKey).then((jwk) => { jwk.kid = 'key1' @@ -29,8 +32,8 @@ const jwk2 = await jose.exportJWK(keys2.publicKey).then((jwk) => { const jwks = jose.createLocalJWKSet({ keys: [jwk1, jwk2] }) -console.log('keys\n', await jose.exportPKCS8(keys.privateKey)) -console.log('keys2\n', await jose.exportPKCS8(keys2.privateKey)) +consola.log('keys\n', await jose.exportPKCS8(keys.privateKey)) +consola.log('keys2\n', await jose.exportPKCS8(keys2.privateKey)) const jwt = await new jose.SignJWT({ foo: 'bar', @@ -42,10 +45,10 @@ const jwt = await new jose.SignJWT({ .setIssuedAt() .sign(keys.privateKey) -console.log('jwt', jwt) +consola.log('jwt', jwt) const { payload, protectedHeader } = await jose .jwtVerify(jwt, jwks, options) -console.log(protectedHeader) -console.log(payload) +consola.log(protectedHeader) +consola.log(payload) diff --git a/lib/jose/keys.ts b/lib/jose/keys.ts new file mode 100644 index 0000000..b294501 --- /dev/null +++ b/lib/jose/keys.ts @@ -0,0 +1,24 @@ +import { readFile } from 'node:fs/promises' + +import * as jose from 'jose' + +import type { KeyPair } from './types' + +export const keys1: KeyPair = JSON.parse(await readFile( + 'keys/key1.jwk', + 'utf8', +)) +export const keys2: KeyPair = JSON.parse(await readFile( + 'keys/key2.jwk', + 'utf8', +)) + +export const keys1Private = await jose.importJWK(keys1.privateKey) +export const keys2Private = await jose.importJWK(keys2.privateKey) + +export const jwks = jose.createLocalJWKSet({ + keys: [ + keys1.publicKey, + keys2.publicKey, + ], +}) diff --git a/lib/jose/sign.ts b/lib/jose/sign.ts new file mode 100644 index 0000000..0e38ea3 --- /dev/null +++ b/lib/jose/sign.ts @@ -0,0 +1,41 @@ +import * as jose from 'jose' + +import { keys1Private } from './keys' + +import type { KeyPair } from './types' + +const alg = 'ES256' +const options = { + issuer: 'urn:example:issuer', + audience: 'urn:example:audience', +} + +export async function sign( + keyPair: KeyPair, + payload: jose.JWTPayload, +) { + return new jose.SignJWT(payload) + .setIssuer(options.issuer) + .setAudience(options.audience) + .setProtectedHeader({ + alg, + kid: 'key1', + }) + .setExpirationTime('10m') + .setIssuedAt() + .sign(keys1Private) +} + +export async function verify( + jwt: string, + keySet: jose.JWTVerifyGetKey, + verifyOptions?: jose.VerifyOptions, +) { + const { payload } = await jose.jwtVerify( + jwt, + keySet, + verifyOptions, + ) + + return payload +} diff --git a/lib/jose/types.ts b/lib/jose/types.ts new file mode 100644 index 0000000..65e2fca --- /dev/null +++ b/lib/jose/types.ts @@ -0,0 +1,5 @@ +import type * as jose from 'jose' + +export type KeyPair = jose.GenerateKeyPairResult< + jose.KeyLike & jose.JWK +> diff --git a/lib/ws/client.ts b/lib/ws/client.ts index c4b39ce..6d2555d 100644 --- a/lib/ws/client.ts +++ b/lib/ws/client.ts @@ -1,11 +1,13 @@ -import { sleep } from '@antfu/utils' -import consola from 'consola' import { WebSocket } from 'ws' +import { jwks, keys1 } from '@/jose/keys' +import { sign, verify } from '@/jose/sign' + import type { Buffer } from 'node:buffer' import type { ClientOptions } from 'ws' -const logger = consola.withTag('client/ws') +// import consola from 'consola' +// const logger = consola.withTag('client/ws') type BufferLike = | string @@ -54,10 +56,10 @@ export class WebSocketProxy extends WebSocket { ) { this.on(event, async (...args: any[]) => { if (event === 'message') { - await sleep(100) - logger.debug('Receiving', event, JSON.parse(args[0] as string)) const [data, isBinary] = args as [BufferLike, boolean] - listener.call(this, data, isBinary) + + const jws = await verify(data.toString(), jwks) + listener.call(this, JSON.stringify(jws), isBinary) return } @@ -66,10 +68,12 @@ export class WebSocketProxy extends WebSocket { }) } - private async customSend(data: BufferLike, cb?: (error?: Error) => void) { - await sleep(100) - logger.debug('Sending', data) + private async customSend( + data: BufferLike, + cb?: (error?: Error) => void, + ) { + const jws = await sign(keys1, JSON.parse(data.toString())) - this.send(data, cb) + this.send(jws, cb) } } diff --git a/lib/ws/server.ts b/lib/ws/server.ts index 4789473..425d727 100644 --- a/lib/ws/server.ts +++ b/lib/ws/server.ts @@ -1,7 +1,9 @@ -import { sleep } from '@antfu/utils' import consola from 'consola' import { WebSocketServer } from 'ws' +import { jwks, keys2 } from '@/jose/keys' +import { sign, verify } from '@/jose/sign' + import type { Buffer } from 'node:buffer' import type { IncomingMessage } from 'node:http' import type { ServerOptions, WebSocket } from 'ws' @@ -19,12 +21,12 @@ type BufferLike = | SharedArrayBuffer | readonly any[] | readonly number[] - | { valueOf(): ArrayBuffer } - | { valueOf(): SharedArrayBuffer } - | { valueOf(): Uint8Array } - | { valueOf(): readonly number[] } - | { valueOf(): string } - | { [Symbol.toPrimitive](hint: string): string } + | { valueOf: () => ArrayBuffer } + | { valueOf: () => SharedArrayBuffer } + | { valueOf: () => Uint8Array } + | { valueOf: () => readonly number[] } + | { valueOf: () => string } + | { [Symbol.toPrimitive]: (hint: string) => string } export class WebSocketProxy< T extends typeof WebSocket.WebSocket = typeof WebSocket.WebSocket, @@ -84,11 +86,10 @@ export class WebSocketProxy< } if (event === 'message') { - await sleep(100) - logger.info('Receiving', event, JSON.parse(args[0] as string)) - // logger.debug('Receiving', event, JSON.parse(args[0] as string)) const [data, isBinary] = args as [BufferLike, boolean] - listener.call(this, data, isBinary) + const jws = await verify(data.toString(), jwks) + + listener.call(this, JSON.stringify(jws), isBinary) return } @@ -102,12 +103,10 @@ export class WebSocketProxy< data: BufferLike, cb?: (error?: Error) => void, ) { - await sleep(100) - - logger.debug('Sending', data) - if ('send' in this) { - return this.send(data, cb) + const jws = await sign(keys2, JSON.parse(data.toString())) + + return this.send(jws, cb) } } } diff --git a/scripts/keys.ts b/scripts/keys.ts new file mode 100644 index 0000000..6918ada --- /dev/null +++ b/scripts/keys.ts @@ -0,0 +1,36 @@ +import { writeFile } from 'node:fs/promises' + +import * as jose from 'jose' + +const alg = 'ES256' + +const keys1 = await jose.generateKeyPair(alg, { extractable: true }) +const keys2 = await jose.generateKeyPair(alg, { extractable: true }) + +const jwk1 = { + publicKey: await jose.exportJWK(keys1.publicKey).then(addKid('key1')), + privateKey: await jose.exportJWK(keys1.privateKey).then(addKid('key1')), +} + +const jwk2 = { + publicKey: await jose.exportJWK(keys2.publicKey).then(addKid('key2')), + privateKey: await jose.exportJWK(keys2.privateKey).then(addKid('key2')), +} + +await writeFile( + `keys/key1.jwk`, + JSON.stringify(jwk1, null, 2), +) + +await writeFile( + `keys/key2.jwk`, + JSON.stringify(jwk2, null, 2), +) + +function addKid(kid: string) { + return (jwk: jose.JWK) => { + jwk.kid = kid + + return jwk + } +} diff --git a/src/client/proxy.ts b/src/client/proxy.ts index c4b39ce..2db0376 100644 --- a/src/client/proxy.ts +++ b/src/client/proxy.ts @@ -18,15 +18,15 @@ type BufferLike = | SharedArrayBuffer | readonly any[] | readonly number[] - | { valueOf(): ArrayBuffer } - | { valueOf(): SharedArrayBuffer } - | { valueOf(): Uint8Array } - | { valueOf(): readonly number[] } - | { valueOf(): string } - | { [Symbol.toPrimitive](hint: string): string } + | { valueOf: () => ArrayBuffer } + | { valueOf: () => SharedArrayBuffer } + | { valueOf: () => Uint8Array } + | { valueOf: () => readonly number[] } + | { valueOf: () => string } + | { [Symbol.toPrimitive]: (hint: string) => string } export class WebSocketProxy extends WebSocket { - constructor( + public constructor( address: string | URL, protocols?: string | string[], options?: ClientOptions, diff --git a/src/workers/test.ts b/src/workers/test.ts new file mode 100644 index 0000000..bbe9d63 --- /dev/null +++ b/src/workers/test.ts @@ -0,0 +1,36 @@ +interface TestType { + [key: string]: string[] +} + +function generateCombinations(test: { [key: string]: string[] }): string[][] { + const keys = Object.keys(test) + const combinations: string[][] = [] + + const generate = (current: string[], index: number): void => { + if (index === keys.length) { + combinations.push(current) + + return + } + + const key = keys[index] + const values = test[key] + for (let i = 0; i < values.length; i++) { + const newCurrent = current.concat(`${key}-${values[i]}`) + generate(newCurrent, index + 1) + } + } + + generate([], 0) + + return combinations +} + +const test: TestType = { + size: ['xs', 's', 'm', 'l', 'xl'], + color: ['red', 'green', 'blue'], + material: ['wood', 'plastic', 'metal'], +} + +const result = generateCombinations(test) +console.log(result)