Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Commit 81e0a23

Browse files
fix: pad base64 strings for base64js (#722)
* fix: pad base64 strings for base64js * really test signer for Node
1 parent e53acfb commit 81e0a23

File tree

4 files changed

+178
-7
lines changed

4 files changed

+178
-7
lines changed

browser-test/test.crypto.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as base64js from 'base64-js';
2+
import {assert} from 'chai';
3+
import {createCrypto} from '../src/crypto/crypto';
4+
import {BrowserCrypto} from '../src/crypto/browser/crypto';
5+
6+
// The following public key was copied from JWK RFC 7517:
7+
// https://tools.ietf.org/html/rfc7517
8+
// The private key used for signing the test message below was taken from the same RFC.
9+
const publicKey = {
10+
kty: 'RSA',
11+
n:
12+
'0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw',
13+
e: 'AQAB',
14+
alg: 'RS256',
15+
kid: '2011-04-29',
16+
};
17+
18+
// Not all browsers support `TextEncoder`. The following `require` will
19+
// provide a fast UTF8-only replacement for those browsers that don't support
20+
// text encoding natively.
21+
require('fast-text-encoding');
22+
23+
describe('Browser crypto tests', () => {
24+
const crypto = createCrypto();
25+
26+
it('should create a BrowserCrypto instance', () => {
27+
assert(crypto instanceof BrowserCrypto);
28+
});
29+
30+
it('should calculate SHA256 digest', async () => {
31+
const input = 'I can calculate SHA256';
32+
const expectedDigest = 'c9CEhti/1PtLwS3YkDYE3b3lrZW276VnvXI86BqIESI=';
33+
const calculatedDigest = await crypto.sha256DigestBase64(input);
34+
assert.strictEqual(calculatedDigest, expectedDigest);
35+
});
36+
37+
it('should generate random bytes', () => {
38+
const requestedLength = 20;
39+
const generated1Base64 = crypto.randomBytesBase64(requestedLength);
40+
const generated1 = base64js.toByteArray(generated1Base64);
41+
assert.strictEqual(generated1.length, requestedLength);
42+
const generated2Base64 = crypto.randomBytesBase64(requestedLength);
43+
const generated2 = base64js.toByteArray(generated2Base64);
44+
assert.strictEqual(generated2.length, requestedLength);
45+
// random strings are random! let's just check they are different.
46+
// if they are the same, we have a problem.
47+
assert.notStrictEqual(generated1Base64, generated2Base64);
48+
});
49+
50+
it('should verify a signature', async () => {
51+
const message = 'This message is signed';
52+
const signatureBase64 = [
53+
'BE1qD48LdssePdMmOhcanOd8V+i4yLSOL0H2EXNyy',
54+
'lCePnldIsLVqrOJnVkd0MUKxS/Y9B0te2tqlS8psP',
55+
'j9IWjcpiQeT9wUDRadxHIX26W6JHgSCOzOavpJCbh',
56+
'M3Kez7QEwbkrI54rYu7qgx/mmckxkC0vhg0Z5OQbO',
57+
'IXfILVs1ztNNdt9r/ZzNVxTMKhL3nHLfjVqG/LUGy',
58+
'RhFhjzLvIJAfL0CSEfycUvm6t5NVzF4SkZ8KKQ7wJ',
59+
'vLw492bRB/633GJOZ1prVjAUQUI64BXFrvRgWsxLK',
60+
'M0XtF5tNbC+eIDrH0LiMraAhcZwj1iWofH1h/dg3E',
61+
'xtU9UWfbed/yfw',
62+
].join(''); // note: no padding
63+
const verified = await crypto.verify(publicKey, message, signatureBase64);
64+
assert(verified);
65+
});
66+
67+
it('should not createSign', () => {
68+
assert.throws(() => {
69+
crypto.createSign('never worked');
70+
});
71+
});
72+
73+
it('should decode unpadded base64', () => {
74+
const originalString = 'test string';
75+
const base64String = 'dGVzdCBzdHJpbmc';
76+
const decodedString = crypto.decodeBase64StringUtf8(base64String);
77+
assert.strictEqual(decodedString, originalString);
78+
});
79+
80+
it('should encode to base64 and pad the result', () => {
81+
const originalString = 'test string';
82+
const base64String = 'dGVzdCBzdHJpbmc=';
83+
const encodedString = crypto.encodeBase64StringUtf8(originalString);
84+
assert.strictEqual(encodedString, base64String);
85+
});
86+
});

src/crypto/browser/crypto.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ export class BrowserCrypto implements Crypto {
6767
return base64js.fromByteArray(array);
6868
}
6969

70+
private static padBase64(base64: string): string {
71+
// base64js requires padding, so let's add some '='
72+
while (base64.length % 4 !== 0) {
73+
base64 += '=';
74+
}
75+
return base64;
76+
}
77+
7078
async verify(
7179
pubkey: JwkCertificate,
7280
data: string,
@@ -77,11 +85,9 @@ export class BrowserCrypto implements Crypto {
7785
hash: {name: 'SHA-256'},
7886
};
7987
const dataArray = new TextEncoder().encode(data);
80-
// base64js requires padding, so let's add some '='
81-
while (signature.length % 4 !== 0) {
82-
signature += '=';
83-
}
84-
const signatureArray = base64js.toByteArray(signature);
88+
const signatureArray = base64js.toByteArray(
89+
BrowserCrypto.padBase64(signature)
90+
);
8591
const cryptoKey = await window.crypto.subtle.importKey(
8692
'jwk',
8793
pubkey,
@@ -106,7 +112,7 @@ export class BrowserCrypto implements Crypto {
106112
}
107113

108114
decodeBase64StringUtf8(base64: string): string {
109-
const uint8array = base64js.toByteArray(base64);
115+
const uint8array = base64js.toByteArray(BrowserCrypto.padBase64(base64));
110116
const result = new TextDecoder().decode(uint8array);
111117
return result;
112118
}

src/crypto/crypto.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {create} from 'domain';
1817
import {isBrowser} from '../isbrowser';
1918
import {BrowserCrypto} from './browser/crypto';
2019
import {NodeCrypto} from './node/crypto';

test/test.crypto.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as nativeCrypto from 'crypto';
2+
import * as fs from 'fs';
3+
import {assert} from 'chai';
4+
import {createCrypto} from '../src/crypto/crypto';
5+
import {NodeCrypto} from '../src/crypto/node/crypto';
6+
7+
const publicKey = fs.readFileSync('./test/fixtures/public.pem', 'utf-8');
8+
const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8');
9+
10+
describe('Node.js crypto tests', () => {
11+
const crypto = createCrypto();
12+
13+
it('should create a NodeCrypto instance', () => {
14+
assert(crypto instanceof NodeCrypto);
15+
});
16+
17+
it('should calculate SHA256 digest', async () => {
18+
const input = 'I can calculate SHA256';
19+
const expectedDigest = 'c9CEhti/1PtLwS3YkDYE3b3lrZW276VnvXI86BqIESI=';
20+
const calculatedDigest = await crypto.sha256DigestBase64(input);
21+
assert.strictEqual(calculatedDigest, expectedDigest);
22+
});
23+
24+
it('should generate random bytes', () => {
25+
const requestedLength = 20;
26+
const generated1Base64 = crypto.randomBytesBase64(requestedLength);
27+
const generated1 = Buffer.from(generated1Base64, 'base64');
28+
assert.strictEqual(generated1.length, requestedLength);
29+
const generated2Base64 = crypto.randomBytesBase64(requestedLength);
30+
const generated2 = Buffer.from(generated2Base64, 'base64');
31+
assert.strictEqual(generated2.length, requestedLength);
32+
// random strings are random! let's just check they are different.
33+
// if they are the same, we have a problem.
34+
assert.notStrictEqual(generated1Base64, generated2Base64);
35+
});
36+
37+
it('should verify a signature', async () => {
38+
const message = 'This message is signed';
39+
const signatureBase64 = [
40+
'ufyKBV+Ar7Yq8CSmSIN9m38ch4xnWBz8CP4qHh6V+',
41+
'm4cCbeXdR1MEmWVhNJjZQFv3KL3tDAnl0Q4bTcSR/',
42+
'mmhXaRjdxyJ6xAUp0KcbVq6xsDIbnnYHSgYr3zVoS',
43+
'dRRefWSWTknN1S69fNmKEfUeBIJA93xitr3pbqtLC',
44+
'bP28XNU',
45+
].join(''); // note: no padding
46+
const verified = await crypto.verify(publicKey, message, signatureBase64);
47+
assert(verified);
48+
});
49+
50+
it('should create a signer that works', () => {
51+
const message = 'This message is signed';
52+
const expectedSignatureBase64 = [
53+
'ufyKBV+Ar7Yq8CSmSIN9m38ch4xnWBz8CP4qHh6V+',
54+
'm4cCbeXdR1MEmWVhNJjZQFv3KL3tDAnl0Q4bTcSR/',
55+
'mmhXaRjdxyJ6xAUp0KcbVq6xsDIbnnYHSgYr3zVoS',
56+
'dRRefWSWTknN1S69fNmKEfUeBIJA93xitr3pbqtLC',
57+
'bP28XNU=',
58+
].join('');
59+
60+
const signer = crypto.createSign('SHA256');
61+
assert(signer);
62+
signer.update(message);
63+
const signatureBase64 = signer.sign(privateKey, 'base64');
64+
assert.strictEqual(signatureBase64, expectedSignatureBase64);
65+
});
66+
67+
it('should decode unpadded base64', () => {
68+
const originalString = 'test string';
69+
const base64String = 'dGVzdCBzdHJpbmc';
70+
const decodedString = crypto.decodeBase64StringUtf8(base64String);
71+
assert.strictEqual(decodedString, originalString);
72+
});
73+
74+
it('should encode to base64 and pad the result', () => {
75+
const originalString = 'test string';
76+
const base64String = 'dGVzdCBzdHJpbmc=';
77+
const encodedString = crypto.encodeBase64StringUtf8(originalString);
78+
assert.strictEqual(encodedString, base64String);
79+
});
80+
});

0 commit comments

Comments
 (0)