Files
discordeno/packages/utils/src/base64.ts
T
FlenyandAwesome Stickz 3d2ff13e4d feat(oauth2): PKCE support (#4425)
* feat(utils): Add base64url encoding

RFC 7636 (Proof Key for Code Exchange by OAuth Public Clients) requires to encode the data with base64url encoding.

We don't support decoding as we don't need it for PKCE.

* feat(oauth2): Add PKCE support

* make PKCE code verifier optional, fixup comments

---------

Co-authored-by: Awesome Stickz <awesome@stickz.dev>
2025-10-27 19:43:55 +01:00

235 lines
4.7 KiB
TypeScript

/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation
* @param data
*/
export function encode(data: Uint8Array | ArrayBuffer | string): string {
const uint8 = typeof data === 'string' ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data)
return _encode(uint8, base64abc, false)
}
/**
* Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64url representation
* @param data The data to encode
* @returns The base64url encoded string
*/
export function encodeBase64Url(data: Uint8Array | ArrayBuffer | string): string {
const uint8 = typeof data === 'string' ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data)
return _encode(uint8, base64urlAbc, true)
}
/** @private */
function _encode(data: Uint8Array, alpha: string[], skipPadding: boolean): string {
let result = ''
let i
const l = data.length
for (i = 2; i < l; i += 3) {
result += alpha[data[i - 2] >> 2]
result += alpha[((data[i - 2] & 0x03) << 4) | (data[i - 1] >> 4)]
result += alpha[((data[i - 1] & 0x0f) << 2) | (data[i] >> 6)]
result += alpha[data[i] & 0x3f]
}
if (i === l + 1) {
// 1 octet yet to write
result += alpha[data[i - 2] >> 2]
result += alpha[(data[i - 2] & 0x03) << 4]
if (!skipPadding) result += '=='
}
if (i === l) {
// 2 octets yet to write
result += alpha[data[i - 2] >> 2]
result += alpha[((data[i - 2] & 0x03) << 4) | (data[i - 1] >> 4)]
result += alpha[(data[i - 1] & 0x0f) << 2]
if (!skipPadding) result += '='
}
return result
}
/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* Decodes RFC4648 base64 string into an Uint8Array
* @param data
*/
export function decode(data: string): Uint8Array {
if (data.length % 4 !== 0) {
throw new Error('Unable to parse base64 string.')
}
const index = data.indexOf('=')
if (index !== -1 && index < data.length - 2) {
throw new Error('Unable to parse base64 string.')
}
const missingOctets = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0
const n = data.length
const result = new Uint8Array(3 * (n / 4))
let buffer
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
buffer =
(getBase64Code(data.charCodeAt(i)) << 18) |
(getBase64Code(data.charCodeAt(i + 1)) << 12) |
(getBase64Code(data.charCodeAt(i + 2)) << 6) |
getBase64Code(data.charCodeAt(i + 3))
result[j] = buffer >> 16
result[j + 1] = (buffer >> 8) & 0xff
result[j + 2] = buffer & 0xff
}
return result.subarray(0, result.length - missingOctets)
}
/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* @param charCode
*/
function getBase64Code(charCode: number): number {
if (charCode >= base64codes.length) {
throw new Error('Unable to parse base64 string.')
}
const code = base64codes[charCode]
if (code === 255) {
throw new Error('Unable to parse base64 string.')
}
return code
}
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
const base64abc = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'+',
'/',
]
const base64urlAbc = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'-',
'_',
]
// CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
const base64codes = [
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255,
0, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
]