Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix padding from hex string #228

Merged
merged 6 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 33 additions & 92 deletions src/sdk/util/base64.ts
Original file line number Diff line number Diff line change
@@ -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));
}
6 changes: 3 additions & 3 deletions src/sdk/util/ssi/did-jwk.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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}`;

Expand Down Expand Up @@ -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<DIDResolutionResult> {
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);
Expand Down
42 changes: 42 additions & 0 deletions test/util/base64.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});