From 81e0a23da246e62dbd19d9a168065d847c337fb1 Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Wed, 29 May 2019 14:01:29 -0700 Subject: [PATCH] fix: pad base64 strings for base64js (#722) * fix: pad base64 strings for base64js * really test signer for Node --- browser-test/test.crypto.ts | 86 ++++++++++++++++++++++++++++++++++++ src/crypto/browser/crypto.ts | 18 +++++--- src/crypto/crypto.ts | 1 - test/test.crypto.ts | 80 +++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 browser-test/test.crypto.ts create mode 100644 test/test.crypto.ts diff --git a/browser-test/test.crypto.ts b/browser-test/test.crypto.ts new file mode 100644 index 00000000..949577b6 --- /dev/null +++ b/browser-test/test.crypto.ts @@ -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); + }); +}); diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index 24c3de34..3c96f39c 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -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, @@ -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, @@ -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; } diff --git a/src/crypto/crypto.ts b/src/crypto/crypto.ts index 3683244c..b9f46f23 100644 --- a/src/crypto/crypto.ts +++ b/src/crypto/crypto.ts @@ -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'; diff --git a/test/test.crypto.ts b/test/test.crypto.ts new file mode 100644 index 00000000..112a81d6 --- /dev/null +++ b/test/test.crypto.ts @@ -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); + }); +});