From 6b3e828126542f3c7c33efe94fed0d32821af6fe Mon Sep 17 00:00:00 2001 From: Toby Date: Thu, 18 Jan 2024 22:36:26 +0100 Subject: [PATCH] working on more tests --- src/index.spec.ts | 2 +- src/index.ts | 104 ++++------------------------------------------ src/utils.spec.ts | 81 ++++++++++++++++++++++++++++++++++++ src/utils.ts | 93 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 96 deletions(-) create mode 100644 src/utils.spec.ts create mode 100644 src/utils.ts diff --git a/src/index.spec.ts b/src/index.spec.ts index b9fe6a0..fffd99e 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -122,4 +122,4 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith const verified = await jwt.verify(token, data.public, algorithm) expect(verified).toBeTruthy() }) -}) +}) \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a3a9452..5f29d66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,12 @@ +import { + textToArrayBuffer, + arrayBufferToBase64Url, + base64UrlToArrayBuffer, + textToBase64Url, + importKey, + decodePayload +} from "./utils" + if (typeof crypto === 'undefined' || !crypto.subtle) throw new Error('SubtleCrypto not supported!') @@ -115,101 +124,6 @@ const algorithms: JwtAlgorithms = { RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } } } -function bytesToByteString(bytes: Uint8Array): string { - let byteStr = '' - for (let i = 0; i < bytes.byteLength; i++) { - byteStr += String.fromCharCode(bytes[i]) - } - return byteStr -} - -function byteStringToBytes(byteStr: string): Uint8Array { - let bytes = new Uint8Array(byteStr.length) - for (let i = 0; i < byteStr.length; i++) { - bytes[i] = byteStr.charCodeAt(i) - } - return bytes -} - -function arrayBufferToBase64String(arrayBuffer: ArrayBuffer): string { - return btoa(bytesToByteString(new Uint8Array(arrayBuffer))) -} - -function base64StringToArrayBuffer(b64str: string): ArrayBuffer { - return byteStringToBytes(atob(b64str)).buffer -} - -function textToArrayBuffer(str: string): ArrayBuffer { - return byteStringToBytes(decodeURI(encodeURIComponent(str))) -} - -// @ts-ignore -function arrayBufferToText(arrayBuffer: ArrayBuffer): string { - return bytesToByteString(new Uint8Array(arrayBuffer)) -} - -function arrayBufferToBase64Url(arrayBuffer: ArrayBuffer): string { - return arrayBufferToBase64String(arrayBuffer).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') -} - -function base64UrlToArrayBuffer(b64url: string): ArrayBuffer { - return base64StringToArrayBuffer(b64url.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')) -} - -function textToBase64Url(str: string): string { - const encoder = new TextEncoder(); - const charCodes = encoder.encode(str); - const binaryStr = String.fromCharCode(...charCodes); - return btoa(binaryStr).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') -} - -function pemToBinary(pem: string): ArrayBuffer { - return base64StringToArrayBuffer(pem.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, '')) -} - -async function importTextSecret(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { - return await crypto.subtle.importKey("raw", textToArrayBuffer(key), algorithm, true, ["verify", "sign"]) -} - -async function importJwk(key: JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { - return await crypto.subtle.importKey("jwk", key, algorithm, true, ["verify", "sign"]) -} - -async function importPublicKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { - return await crypto.subtle.importKey("spki", pemToBinary(key), algorithm, true, ["verify"]) -} - -async function importPrivateKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { - return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, ["sign"]) -} - -async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { - if (typeof key === 'object') - return importJwk(key, algorithm) - - if (typeof key !== 'string') - throw new Error('Unsupported key type!') - - if (key.includes('PUBLIC')) - return importPublicKey(key, algorithm) - - if (key.includes('PRIVATE')) - return importPrivateKey(key, algorithm) - - return importTextSecret(key, algorithm) -} - -function decodePayload(raw: string): T | undefined { - try { - const bytes = Array.from(atob(raw), char => char.charCodeAt(0)); - const decodedString = new TextDecoder('utf-8').decode(new Uint8Array(bytes)); - - return JSON.parse(decodedString); - } catch { - return - } -} - /** * Signs a payload and returns the token * diff --git a/src/utils.spec.ts b/src/utils.spec.ts new file mode 100644 index 0000000..56b02bd --- /dev/null +++ b/src/utils.spec.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from '@jest/globals' +import { + bytesToByteString, + byteStringToBytes, + arrayBufferToBase64String, + base64StringToArrayBuffer, + textToArrayBuffer, + arrayBufferToText, + arrayBufferToBase64Url, + base64UrlToArrayBuffer, + textToBase64Url, + pemToBinary, + importTextSecret +} from './utils' + +describe('Converters', () => { + const testString = 'cloudflare-worker-jwt' + const testByteArray = [ 99, 108, 111, 117, 100, 102, 108, 97, 114, 101, 45, 119, 111, 114, 107, 101, 114, 45, 106, 119, 116 ] + const testUint8Array = new Uint8Array(testByteArray) + const testBase64String = 'Y2xvdWRmbGFyZS13b3JrZXItand0' + const testArrayBuffer = testUint8Array.buffer + + test('bytesToByteString', () => { + expect(bytesToByteString(testUint8Array)).toStrictEqual(testString) + }) + + test('byteStringToBytes', () => { + expect(byteStringToBytes(testString)).toStrictEqual(testUint8Array) + }) + + test('arrayBufferToBase64String', () => { + expect(arrayBufferToBase64String(testArrayBuffer)).toStrictEqual(testBase64String) + }) + + test('base64StringToArrayBuffer', () => { + expect(base64StringToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer) + }) + + test('textToArrayBuffer', () => { + expect(textToArrayBuffer(testString)).toStrictEqual(testUint8Array) + }) + + test('arrayBufferToText', () => { + expect(arrayBufferToText(testArrayBuffer)).toStrictEqual(testString) + }) + + test('arrayBufferToBase64Url', () => { + expect(arrayBufferToBase64Url(testArrayBuffer)).toStrictEqual(testBase64String) + }) + + test('base64UrlToArrayBuffer', () => { + expect(base64UrlToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer) + }) + + test('textToBase64Url', () => { + expect(textToBase64Url(testString)).toStrictEqual(testBase64String) + }) + + test('pemToBinary', () => { + expect(pemToBinary(`-----BEGIN PUBLIC KEY-----\n${testBase64String}\n-----END PUBLIC KEY-----`)).toStrictEqual(testArrayBuffer) + }) +}) + +describe('Imports', () => { + test('importTextSecret', async () => { + const testKey = 'cloudflare-worker-jwt' + const testAlgorithm = { name: 'HMAC', hash: { name: 'SHA-256' } } + const testCryptoKey = { type: 'secret', extractable: true, algorithm: { ...testAlgorithm, length: 168 }, usages: ['verify', 'sign'] } + + expect(await importTextSecret(testKey, testAlgorithm)).toMatchObject(testCryptoKey) + }) + + //test('importJwk', async () => {}) + //test('importPublicKey', async () => {}) + //test('importPrivateKey', async () => {}) + //test('importKey', async () => {}) +}) + +//describe('Payload', () => { +// test('decodePayload', () => {}) +//}) \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..cce55f8 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,93 @@ +export function bytesToByteString(bytes: Uint8Array): string { + let byteStr = '' + for (let i = 0; i < bytes.byteLength; i++) { + byteStr += String.fromCharCode(bytes[i]) + } + return byteStr +} + +export function byteStringToBytes(byteStr: string): Uint8Array { + let bytes = new Uint8Array(byteStr.length) + for (let i = 0; i < byteStr.length; i++) { + bytes[i] = byteStr.charCodeAt(i) + } + return bytes +} + +export function arrayBufferToBase64String(arrayBuffer: ArrayBuffer): string { + return btoa(bytesToByteString(new Uint8Array(arrayBuffer))) +} + +export function base64StringToArrayBuffer(b64str: string): ArrayBuffer { + return byteStringToBytes(atob(b64str)).buffer +} + +export function textToArrayBuffer(str: string): ArrayBuffer { + return byteStringToBytes(decodeURI(encodeURIComponent(str))) +} + +export function arrayBufferToText(arrayBuffer: ArrayBuffer): string { + return bytesToByteString(new Uint8Array(arrayBuffer)) +} + +export function arrayBufferToBase64Url(arrayBuffer: ArrayBuffer): string { + return arrayBufferToBase64String(arrayBuffer).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') +} + +export function base64UrlToArrayBuffer(b64url: string): ArrayBuffer { + return base64StringToArrayBuffer(b64url.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')) +} + +export function textToBase64Url(str: string): string { + const encoder = new TextEncoder(); + const charCodes = encoder.encode(str); + const binaryStr = String.fromCharCode(...charCodes); + return btoa(binaryStr).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') +} + +export function pemToBinary(pem: string): ArrayBuffer { + return base64StringToArrayBuffer(pem.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, '')) +} + +export async function importTextSecret(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { + return await crypto.subtle.importKey("raw", textToArrayBuffer(key), algorithm, true, ["verify", "sign"]) +} + +export async function importJwk(key: JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { + return await crypto.subtle.importKey("jwk", key, algorithm, true, ["verify", "sign"]) +} + +export async function importPublicKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { + return await crypto.subtle.importKey("spki", pemToBinary(key), algorithm, true, ["verify"]) +} + +export async function importPrivateKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { + return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, ["sign"]) +} + +export async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise { + if (typeof key === 'object') + return importJwk(key, algorithm) + + if (typeof key !== 'string') + throw new Error('Unsupported key type!') + + if (key.includes('PUBLIC')) + return importPublicKey(key, algorithm) + + if (key.includes('PRIVATE')) + return importPrivateKey(key, algorithm) + + return importTextSecret(key, algorithm) +} + +export function decodePayload(raw: string): T | undefined { + try { + const bytes = Array.from(atob(raw), char => char.charCodeAt(0)); + const decodedString = new TextDecoder('utf-8').decode(new Uint8Array(bytes)); + + return JSON.parse(decodedString); + } catch { + return + } +} \ No newline at end of file