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: pad base64 strings for base64js #722

Merged
merged 4 commits into from
May 29, 2019
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
86 changes: 86 additions & 0 deletions browser-test/test.crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as base64js from 'base64-js';
import {assert} from 'chai';
import {createCrypto} from '../src/crypto/crypto';
import {BrowserCrypto} from '../src/crypto/browser/crypto';

// The following public key was copied from JWK RFC 7517:
// https://tools.ietf.org/html/rfc7517
// The private key used for signing the test message below was taken from the same RFC.
const publicKey = {
kty: 'RSA',
n:
'0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw',
e: 'AQAB',
alg: 'RS256',
kid: '2011-04-29',
};

// Not all browsers support `TextEncoder`. The following `require` will
// provide a fast UTF8-only replacement for those browsers that don't support
// text encoding natively.
require('fast-text-encoding');

describe('Browser crypto tests', () => {
const crypto = createCrypto();

it('should create a BrowserCrypto instance', () => {
assert(crypto instanceof BrowserCrypto);
});

it('should calculate SHA256 digest', async () => {
const input = 'I can calculate SHA256';
const expectedDigest = 'c9CEhti/1PtLwS3YkDYE3b3lrZW276VnvXI86BqIESI=';
const calculatedDigest = await crypto.sha256DigestBase64(input);
assert.strictEqual(calculatedDigest, expectedDigest);
});

it('should generate random bytes', () => {
const requestedLength = 20;
const generated1Base64 = crypto.randomBytesBase64(requestedLength);
const generated1 = base64js.toByteArray(generated1Base64);
assert.strictEqual(generated1.length, requestedLength);
const generated2Base64 = crypto.randomBytesBase64(requestedLength);
const generated2 = base64js.toByteArray(generated2Base64);
assert.strictEqual(generated2.length, requestedLength);
// random strings are random! let's just check they are different.
// if they are the same, we have a problem.
assert.notStrictEqual(generated1Base64, generated2Base64);
});

it('should verify a signature', async () => {
const message = 'This message is signed';
const signatureBase64 = [
'BE1qD48LdssePdMmOhcanOd8V+i4yLSOL0H2EXNyy',
'lCePnldIsLVqrOJnVkd0MUKxS/Y9B0te2tqlS8psP',
'j9IWjcpiQeT9wUDRadxHIX26W6JHgSCOzOavpJCbh',
'M3Kez7QEwbkrI54rYu7qgx/mmckxkC0vhg0Z5OQbO',
'IXfILVs1ztNNdt9r/ZzNVxTMKhL3nHLfjVqG/LUGy',
'RhFhjzLvIJAfL0CSEfycUvm6t5NVzF4SkZ8KKQ7wJ',
'vLw492bRB/633GJOZ1prVjAUQUI64BXFrvRgWsxLK',
'M0XtF5tNbC+eIDrH0LiMraAhcZwj1iWofH1h/dg3E',
'xtU9UWfbed/yfw',
].join(''); // note: no padding
const verified = await crypto.verify(publicKey, message, signatureBase64);
assert(verified);
});

it('should not createSign', () => {
assert.throws(() => {
crypto.createSign('never worked');
});
});

it('should decode unpadded base64', () => {
const originalString = 'test string';
const base64String = 'dGVzdCBzdHJpbmc';
const decodedString = crypto.decodeBase64StringUtf8(base64String);
assert.strictEqual(decodedString, originalString);
});

it('should encode to base64 and pad the result', () => {
const originalString = 'test string';
const base64String = 'dGVzdCBzdHJpbmc=';
const encodedString = crypto.encodeBase64StringUtf8(originalString);
assert.strictEqual(encodedString, base64String);
});
});
18 changes: 12 additions & 6 deletions src/crypto/browser/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export class BrowserCrypto implements Crypto {
return base64js.fromByteArray(array);
}

private static padBase64(base64: string): string {
// base64js requires padding, so let's add some '='
while (base64.length % 4 !== 0) {
base64 += '=';
}
return base64;
}

async verify(
pubkey: JwkCertificate,
data: string,
Expand All @@ -77,11 +85,9 @@ export class BrowserCrypto implements Crypto {
hash: {name: 'SHA-256'},
};
const dataArray = new TextEncoder().encode(data);
// base64js requires padding, so let's add some '='
while (signature.length % 4 !== 0) {
signature += '=';
}
const signatureArray = base64js.toByteArray(signature);
const signatureArray = base64js.toByteArray(
BrowserCrypto.padBase64(signature)
);
const cryptoKey = await window.crypto.subtle.importKey(
'jwk',
pubkey,
Expand All @@ -106,7 +112,7 @@ export class BrowserCrypto implements Crypto {
}

decodeBase64StringUtf8(base64: string): string {
const uint8array = base64js.toByteArray(base64);
const uint8array = base64js.toByteArray(BrowserCrypto.padBase64(base64));
const result = new TextDecoder().decode(uint8array);
return result;
}
Expand Down
1 change: 0 additions & 1 deletion src/crypto/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import {create} from 'domain';
import {isBrowser} from '../isbrowser';
import {BrowserCrypto} from './browser/crypto';
import {NodeCrypto} from './node/crypto';
Expand Down
80 changes: 80 additions & 0 deletions test/test.crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as nativeCrypto from 'crypto';
import * as fs from 'fs';
import {assert} from 'chai';
import {createCrypto} from '../src/crypto/crypto';
import {NodeCrypto} from '../src/crypto/node/crypto';

const publicKey = fs.readFileSync('./test/fixtures/public.pem', 'utf-8');
const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8');

describe('Node.js crypto tests', () => {
const crypto = createCrypto();

it('should create a NodeCrypto instance', () => {
assert(crypto instanceof NodeCrypto);
});

it('should calculate SHA256 digest', async () => {
const input = 'I can calculate SHA256';
const expectedDigest = 'c9CEhti/1PtLwS3YkDYE3b3lrZW276VnvXI86BqIESI=';
const calculatedDigest = await crypto.sha256DigestBase64(input);
assert.strictEqual(calculatedDigest, expectedDigest);
});

it('should generate random bytes', () => {
const requestedLength = 20;
const generated1Base64 = crypto.randomBytesBase64(requestedLength);
const generated1 = Buffer.from(generated1Base64, 'base64');
assert.strictEqual(generated1.length, requestedLength);
const generated2Base64 = crypto.randomBytesBase64(requestedLength);
const generated2 = Buffer.from(generated2Base64, 'base64');
assert.strictEqual(generated2.length, requestedLength);
// random strings are random! let's just check they are different.
// if they are the same, we have a problem.
assert.notStrictEqual(generated1Base64, generated2Base64);
});

it('should verify a signature', async () => {
const message = 'This message is signed';
const signatureBase64 = [
'ufyKBV+Ar7Yq8CSmSIN9m38ch4xnWBz8CP4qHh6V+',
'm4cCbeXdR1MEmWVhNJjZQFv3KL3tDAnl0Q4bTcSR/',
'mmhXaRjdxyJ6xAUp0KcbVq6xsDIbnnYHSgYr3zVoS',
'dRRefWSWTknN1S69fNmKEfUeBIJA93xitr3pbqtLC',
'bP28XNU',
].join(''); // note: no padding
const verified = await crypto.verify(publicKey, message, signatureBase64);
assert(verified);
});

it('should create a signer that works', () => {
const message = 'This message is signed';
const expectedSignatureBase64 = [
'ufyKBV+Ar7Yq8CSmSIN9m38ch4xnWBz8CP4qHh6V+',
'm4cCbeXdR1MEmWVhNJjZQFv3KL3tDAnl0Q4bTcSR/',
'mmhXaRjdxyJ6xAUp0KcbVq6xsDIbnnYHSgYr3zVoS',
'dRRefWSWTknN1S69fNmKEfUeBIJA93xitr3pbqtLC',
'bP28XNU=',
].join('');

const signer = crypto.createSign('SHA256');
assert(signer);
signer.update(message);
const signatureBase64 = signer.sign(privateKey, 'base64');
assert.strictEqual(signatureBase64, expectedSignatureBase64);
});

it('should decode unpadded base64', () => {
const originalString = 'test string';
const base64String = 'dGVzdCBzdHJpbmc';
const decodedString = crypto.decodeBase64StringUtf8(base64String);
assert.strictEqual(decodedString, originalString);
});

it('should encode to base64 and pad the result', () => {
const originalString = 'test string';
const base64String = 'dGVzdCBzdHJpbmc=';
const encodedString = crypto.encodeBase64StringUtf8(originalString);
assert.strictEqual(encodedString, base64String);
});
});