diff --git a/lib/jws.js b/lib/jws.js index 159016d..698ef28 100644 --- a/lib/jws.js +++ b/lib/jws.js @@ -1,5 +1,5 @@ import { decodeBase64UrlAsArray, decodeBase64UrlAsString, encodeBase64UrlAsString } from '../utils/base64.js'; -import { octetFromUtf8 } from '../utils/utf8.js'; +import { uint8ArrayFromUtf8 } from '../utils/utf8.js'; import KeyStore from './KeyStore.js'; import { decodeProtectedHeader, encodePayload, encodeProtectedHeader } from './jose.js'; @@ -177,7 +177,7 @@ export async function validateSignature(encodedPayload, signatureObject, jwk) { const verified = await KeyStore.default.verify( jwk, decodeBase64UrlAsArray(signature), - Uint8Array.from(octetFromUtf8(signingInput)), + uint8ArrayFromUtf8(signingInput), ); if (verified) return; } catch (e) { console.error(e); } diff --git a/test/lib/jws.js b/test/lib/jws.js index b8b05b7..805203a 100644 --- a/test/lib/jws.js +++ b/test/lib/jws.js @@ -1,7 +1,7 @@ import { createES256JWK, createES512JWK, createHS256JWK, createRS256JWK } from '../../lib/jwa.js'; import { decodeJSON, decodeJSONUnsafe, decodeString, decodeStringUnsafe, decodeUint8Array, decodeUint8ArrayUnsafe, signCompact, signObject, validate } from '../../lib/jws.js'; import { decodeBase64AsString, decodeBase64UrlAsArray, encodeBase64UrlAsString } from '../../utils/base64.js'; -import { octetFromUtf8 } from '../../utils/utf8.js'; +import { uint8ArrayFromUtf8 } from '../../utils/utf8.js'; import test from '../tester.js'; const RFC7515_APPENDIX_A_JWS = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; @@ -344,7 +344,7 @@ test('CVE-2015-9235 - Replicate setup', async (t) => { const header = { alg: 'HS256', typ: 'JWT' }; const payload = { loggedInAs: 'admin', iat: 1_422_779_638 }; const key = 'secretkey'; - const hmacKey = new Uint8Array(octetFromUtf8(key)); + const hmacKey = uint8ArrayFromUtf8(key); const symmetricalJWK = createHS256JWK({ k: hmacKey }); const symmetricalJWS = await signCompact({ protected: header, @@ -360,7 +360,7 @@ test('CVE-2015-9235 - Validation should fail if token specifies alg:none though const header = { alg: 'none', typ: 'JWT' }; const payload = { loggedInAs: 'admin', iat: 1_422_779_638 }; const key = 'secretkey'; - const hmacKey = new Uint8Array(octetFromUtf8(key)); + const hmacKey = uint8ArrayFromUtf8(key); const symmetricalJWK = createHS256JWK({ k: hmacKey }); // Will not allow conflict when signing diff --git a/utils/asn1/encoder.js b/utils/asn1/encoder.js index ffc0e43..a89b4ac 100644 --- a/utils/asn1/encoder.js +++ b/utils/asn1/encoder.js @@ -1,7 +1,7 @@ /* eslint-disable no-bitwise */ import { decodeBase64AsArray } from '../base64.js'; -import { octetFromUtf8 } from '../utf8.js'; +import { uint8ArrayFromUtf8 } from '../utf8.js'; // https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/ @@ -178,17 +178,16 @@ export function encodeDER(tag, entry) { */ export function encodeUTF8String(utf8String) { // ASCII to Hex is the same as UTF8 to HEX (no check) - const entry = [...octetFromUtf8(utf8String)]; - return encodeDER(ASN_TAG.UTF8_STRING, entry); + return encodeDER(ASN_TAG.UTF8_STRING, uint8ArrayFromUtf8(utf8String)); } /** * @param {string} ia5String - * @return {number[]} + * @return {ArrayLike&Iterable} */ export function writeIA5String(ia5String) { // First 128 characters of ASCII (no check) - return [...octetFromUtf8(ia5String)]; + return uint8ArrayFromUtf8(ia5String); } /** @@ -326,8 +325,7 @@ export function encodeBitString(data, length = data.length * 8) { export function encodePrintableString(printableString) { // ASCII to Hex is the same as UTF8 to HEX // No checks here - const entry = [...octetFromUtf8(printableString)]; - return encodeDER(ASN_TAG.PRINTABLE_STRING, entry); + return encodeDER(ASN_TAG.PRINTABLE_STRING, uint8ArrayFromUtf8(printableString)); } /** diff --git a/utils/base64.js b/utils/base64.js index a2b9153..5190411 100644 --- a/utils/base64.js +++ b/utils/base64.js @@ -1,7 +1,7 @@ /* eslint-disable no-bitwise */ // https://datatracker.ietf.org/doc/html/rfc4648#section-4 -import { octetFromUtf8 } from './utf8.js'; +import { uint8ArrayFromUtf8 } from './utf8.js'; const BASE64_CHAR_62 = '+'; const BASE64_CHAR_63 = '/'; @@ -42,7 +42,7 @@ const BIT_MASK_8 = BIT_MASKS.get(8); */ function toIterableUint8(source) { if (typeof source === 'string') { - return octetFromUtf8(source); + return uint8ArrayFromUtf8(source); } if (source instanceof Uint8Array) { return source; diff --git a/utils/crypto.js b/utils/crypto.js index 352f814..8171a52 100644 --- a/utils/crypto.js +++ b/utils/crypto.js @@ -3,7 +3,7 @@ import { parseAlgorithmIdentifier } from '../lib/jwa.js'; -import { octetFromUtf8 } from './utf8.js'; +import { uint8ArrayFromUtf8 } from './utf8.js'; /** @type {Crypto} */ // eslint-disable-next-line unicorn/no-await-expression-member @@ -31,7 +31,7 @@ export async function importJWK(jwk, algorithmIdentifier) { * @return {Promise} */ export async function sign(algorithmIdentifier, key, data) { - const binary = typeof data === 'string' ? Uint8Array.from(octetFromUtf8(data)) : data; + const binary = typeof data === 'string' ? uint8ArrayFromUtf8(data) : data; return await crypto.subtle.sign(algorithmIdentifier, key, binary); } @@ -72,6 +72,6 @@ export async function decrypt(algorithmIdentifier, key, data) { * @return {Promise} */ export async function digest(algorithm, data) { - const binary = typeof data === 'string' ? Uint8Array.from(octetFromUtf8(data)) : data; + const binary = typeof data === 'string' ? uint8ArrayFromUtf8(data) : data; return await crypto.subtle.digest(algorithm, binary); } diff --git a/utils/utf8.js b/utils/utf8.js index 72e71f6..ec35510 100644 --- a/utils/utf8.js +++ b/utils/utf8.js @@ -43,3 +43,15 @@ export function* octetFromUtf8(utf8) { } } } + +/** @type {TextEncoder} */ +let textEncoder; + +/** + * @param {string} utf8 + * @return {Uint8Array} + */ +export function uint8ArrayFromUtf8(utf8) { + textEncoder ??= new TextEncoder(); + return textEncoder.encode(utf8); +}