diff --git a/src/sdk/util/base64.ts b/src/sdk/util/base64.ts index dd787071..ea8365e0 100644 --- a/src/sdk/util/base64.ts +++ b/src/sdk/util/base64.ts @@ -1,118 +1,59 @@ -import { decode, encode } from 'universal-base64url'; +// From utf8 to base64url and visa versa +import { decode as b64UrlDecode, encode as b64UrlEncode } from 'universal-base64url'; +import { decode as b64Decode, encode as b64Encode } from 'universal-base64'; import { BN } from 'bn.js'; +import * as u8a from 'uint8arrays'; -// Inspired by https://github.com/davidchambers/Base64.js/blob/master/base64.js -const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; -const Base64 = { - btoa: (input = '') => { - const str = input; - let output = ''; - - for ( - let block = 0, charCode, i = 0, map = chars; - str.charAt(i | 0) || ((map = '='), i % 1); - output += map.charAt(63 & (block >> (8 - (i % 1) * 8))) - ) { - charCode = str.charCodeAt((i += 3 / 4)); - - if (charCode > 0xff) { - throw new Error( - "'btoa' failed: The string to be encoded contains characters outside of the Latin1 range." - ); - } - - block = (block << 8) | charCode; - } - - return output; - }, - - atob: (input = '') => { - const str = input.replace(/=+$/, ''); - let output = ''; - - if (str.length % 4 === 1) { - throw new Error("'atob' failed: The string to be decoded is not correctly encoded."); - } - - for ( - let bc = 0, bs = 0, buffer, i = 0; - (buffer = str.charAt(i++)); - ~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4) - ? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) - : 0 - ) { - buffer = chars.indexOf(buffer); - } +// Adapted from https://github.com/decentralized-identity/did-jwt/blob/056b2e422896436b781ecab2b466bacf72708d23/src/util.ts +export function bnToBase64Url(bn: typeof BN): string { + const bnString = bn.toString(); + const bi = BigInt(bnString); + const biBytes = bigintToBytes(bi); - return output; - }, -}; + return bytesToBase64(biBytes); +} -// Polyfill for React Native which does not have Buffer, or atob/btoa -// TODO maybe do this at global level? -if (typeof Buffer === 'undefined') { - if (typeof window === 'undefined' || typeof window.atob === 'undefined') { - window.atob = Base64.atob; - window.btoa = Base64.btoa; - } +// Copied from https://github.com/decentralized-identity/did-jwt/blob/056b2e422896436b781ecab2b466bacf72708d23/src/util.ts +export function bytesToBase64(b: Uint8Array): string { + return u8a.toString(b, 'base64pad'); } -export function bnToBase64Url(bn: typeof BN): string { - if (typeof Buffer !== 'undefined') { - // nodejs - const buffer = (bn as any).toArrayLike(Buffer, 'be'); +// Adapted from https://github.com/decentralized-identity/did-jwt/blob/056b2e422896436b781ecab2b466bacf72708d23/src/util.ts +export function bigintToBytes(n: bigint): Uint8Array { + let b64 = n.toString(16); - return Buffer.from(buffer).toString('base64'); - } else { - // browser - return hexToBase64((bn as any).toString('hex')); + // Pad an extra '0' if the hex string is an odd length + if (b64.length % 2 !== 0) { + b64 = `0${b64}`; } -} -function hexToBase64(hexstring: string) { - return window.btoa( - (hexstring as any) - .match(/\w{2}/g) - .map(function (a: string) { - return String.fromCharCode(parseInt(a, 16)); - }) - .join('') - ); + return u8a.fromString(b64, 'base16'); } -export function utf8ToB64(str: string) { - if (typeof Buffer !== 'undefined') { - // nodejs - return Buffer.from(str).toString('base64'); - } else { - // browser - return window.btoa(unescape(encodeURIComponent(str))); - } +// utf8 string to base64 +export function strToBase64(str: string) { + return b64Encode(str); } -export function b64ToUtf8(str: string) { - if (typeof Buffer !== 'undefined') { - // nodejs - return Buffer.from(str, 'base64').toString('utf8'); - } else { - // browser - return decodeURIComponent(escape(window.atob(str))); - } +// base64 to utf8 string +export function base64ToStr(str: string) { + return b64Decode(str); } +// utf8 string to base64url export function strToBase64Url(str: string): string { - return encode(str); + return b64UrlEncode(str); } export function objToBase64Url(obj: object): string { - return encode(JSON.stringify(obj)); + return b64UrlEncode(JSON.stringify(obj)); } +// base64url to utf8 string export function base64UrlToStr(str: string): string { - return decode(str); + return b64UrlDecode(str); } export function base64UrlToObj(str: string): object | any { - return JSON.parse(decode(str)); + return JSON.parse(b64UrlDecode(str)); } diff --git a/src/sdk/util/ssi/did-jwk.ts b/src/sdk/util/ssi/did-jwk.ts index 4c5d41cc..57a4c736 100644 --- a/src/sdk/util/ssi/did-jwk.ts +++ b/src/sdk/util/ssi/did-jwk.ts @@ -1,6 +1,6 @@ import { PublicKey } from '@greymass/eosio'; import { toElliptic } from '../crypto'; -import { b64ToUtf8, bnToBase64Url, utf8ToB64 } from '../base64'; +import { base64ToStr, bnToBase64Url, strToBase64 } from '../base64'; import { ResolverRegistry, ParsedDID, DIDResolutionResult, DIDDocument } from '@tonomy/did-resolver'; export function createJWK(publicKey: PublicKey) { @@ -22,7 +22,7 @@ export function toDid(jwk: any) { // eslint-disable-next-line no-unused-vars const { d, p, q, dp, dq, qi, ...publicKeyJwk } = jwk; // TODO replace with base64url encoder for web - const id = utf8ToB64(JSON.stringify(publicKeyJwk)); + const id = strToBase64(JSON.stringify(publicKeyJwk)); const did = `did:jwk:${id}`; @@ -84,7 +84,7 @@ export function toDidDocument(jwk: any): DIDDocument { // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function resolve(did: any, options = {}): Promise { if (options) options = {}; - const decoded = b64ToUtf8(did.split(':').pop().split('#')[0]); + const decoded = base64ToStr(did.split(':').pop().split('#')[0]); const jwk = JSON.parse(decoded.toString()); const didDoc = toDidDocument(jwk); diff --git a/test/util/base64.test.ts b/test/util/base64.test.ts new file mode 100644 index 00000000..45ae8fb4 --- /dev/null +++ b/test/util/base64.test.ts @@ -0,0 +1,42 @@ +import { base64ToStr, base64UrlToStr, bnToBase64Url, strToBase64Url, strToBase64 } from '../../src/sdk'; +import { BN } from 'bn.js'; + +describe('Base 64()', () => { + it('bnToBase64Url()', () => { + { + // Good BN that does NOT cause error from no padding on the hex value + const bn = new BN('100968908336250941489582664670319762383316987426946165788206218268821633081179'); + const base64 = bnToBase64Url(bn as any); + + expect(base64).toBe('3zpgfkpIN/0k/xkychS26ElYP4Bnb24RcYACzsbzn1s='); + } + + { + // Bad BN that DOES cause error from no padding on the hex value + const bn = new BN('1881146970754576322752261068397796891246589699629597037555588131642783231506'); + const base64 = bnToBase64Url(bn as any); + + expect(base64).toBe('BCixAySH6XqSNMR6MVnd4SCluKq3Ey5RQIy0/0Eu7hI='); + } + }); + + const str = 'hello world'; + const b64 = 'aGVsbG8gd29ybGQ='; + const b64url = 'aGVsbG8gd29ybGQ'; + + it('strToBase64Url()', () => { + const base64url = strToBase64Url(str); + const base64 = strToBase64(str); + + expect(base64url).toBe(b64url); + expect(base64).toBe(b64); + }); + + it('base64UrlToStr()', () => { + const decodedStr1 = base64ToStr(b64); + const decodedStr2 = base64UrlToStr(b64url); + + expect(decodedStr1).toBe(str); + expect(decodedStr2).toBe(str); + }); +});