Skip to content

Commit

Permalink
feat: es crypto features to nodejs and browser (#106)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas.J.Han <lukas.j.han@gmail.com>
Signed-off-by: Mirko Mollik <mirko.mollik@fit.fraunhofer.de>
Signed-off-by: Lukas <Lukas@hopae.io>
Co-authored-by: Mirko Mollik <mirko.mollik@fit.fraunhofer.de>
  • Loading branch information
lukasjhan and cre8 committed Feb 26, 2024
1 parent a8bd120 commit 2d8206e
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 3 deletions.
90 changes: 90 additions & 0 deletions packages/browser-crypto/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,93 @@ export async function digest(
export const getHasher = (algorithm = 'SHA-256') => {
return (data: string) => digest(data, algorithm);
};

export const ES256 = {
alg: 'ES256',

async generateKeyPair() {
const keyPair = await window.crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256', // ES256
},
true, // whether the key is extractable (i.e., can be used in exportKey)
['sign', 'verify'], // can be used to sign and verify signatures
);

// Export the public and private keys in JWK format
const publicKeyJWK = await window.crypto.subtle.exportKey(
'jwk',
keyPair.publicKey,
);
const privateKeyJWK = await window.crypto.subtle.exportKey(
'jwk',
keyPair.privateKey,
);

return { publicKey: publicKeyJWK, privateKey: privateKeyJWK };
},

async getSigner(privateKeyJWK: object) {
const privateKey = await window.crypto.subtle.importKey(
'jwk',
privateKeyJWK,
{
name: 'ECDSA',
namedCurve: 'P-256', // Must match the curve used to generate the key
},
true, // whether the key is extractable (i.e., can be used in exportKey)
['sign'],
);

return async (data: string) => {
const encoder = new TextEncoder();
const signature = await window.crypto.subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-256' }, // Required for ES256
},
privateKey,
encoder.encode(data),
);

return window
.btoa(String.fromCharCode(...new Uint8Array(signature)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, ''); // Convert to base64url format
};
},

async getVerifier(publicKeyJWK: object) {
const publicKey = await window.crypto.subtle.importKey(
'jwk',
publicKeyJWK,
{
name: 'ECDSA',
namedCurve: 'P-256', // Must match the curve used to generate the key
},
true, // whether the key is extractable (i.e., can be used in exportKey)
['verify'],
);

return async (data: string, signatureBase64url: string) => {
const encoder = new TextEncoder();
const signature = Uint8Array.from(
atob(signatureBase64url.replace(/-/g, '+').replace(/_/g, '/')),
(c) => c.charCodeAt(0),
);
const isValid = await window.crypto.subtle.verify(
{
name: 'ECDSA',
hash: { name: 'SHA-256' }, // Required for ES256
},
publicKey,
signature,
encoder.encode(data),
);

return isValid;
};
},
};
25 changes: 23 additions & 2 deletions packages/browser-crypto/src/test/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test, it } from 'vitest';
import { generateSalt, digest, getHasher } from '../index';
import { describe, expect, test } from 'vitest';
import { generateSalt, digest, getHasher, ES256 } from '../index';

// Extract the major version as a number
const nodeVersionMajor = parseInt(
Expand Down Expand Up @@ -41,4 +41,25 @@ describe('This file is for utility functions', () => {
const hash = await getHasher('SHA-512')('test1');
expect(hash).toBeDefined();
});

(nodeVersionMajor < 20 ? test.skip : test)('ES256 alg', () => {
expect(ES256.alg).toBe('ES256');
});

(nodeVersionMajor < 20 ? test.skip : test)('ES256', async () => {
const { privateKey, publicKey } = await ES256.generateKeyPair();
expect(privateKey).toBeDefined();
expect(publicKey).toBeDefined();
expect(typeof privateKey).toBe('object');
expect(typeof publicKey).toBe('object');

const data =
'In cryptography, a salt is random data that is used as an additional input to a one-way function that hashes data, a password or passphrase.';
const signature = await (await ES256.getSigner(privateKey))(data);
expect(signature).toBeDefined();
expect(typeof signature).toBe('string');

const result = await (await ES256.getVerifier(publicKey))(data, signature);
expect(result).toBe(true);
});
});
86 changes: 86 additions & 0 deletions packages/node-crypto/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,89 @@ export const digest = (data: string, algorithm = 'SHA-256'): Uint8Array => {

const toNodeCryptoAlg = (hashAlg: string): string =>
hashAlg.replace('-', '').toLowerCase();

export const ES256 = {
alg: 'ES256',

async generateKeyPair() {
const { subtle } = globalThis.crypto;
const keyPair = await subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256', // ES256
},
true, // whether the key is extractable (i.e., can be used in exportKey)
['sign', 'verify'], // can be used to sign and verify signatures
);

// Export the public and private keys in JWK format
const publicKeyJWK = await subtle.exportKey('jwk', keyPair.publicKey);
const privateKeyJWK = await subtle.exportKey('jwk', keyPair.privateKey);

return { publicKey: publicKeyJWK, privateKey: privateKeyJWK };
},

async getSigner(privateKeyJWK: object) {
const { subtle } = globalThis.crypto;
const privateKey = await subtle.importKey(
'jwk',
privateKeyJWK,
{
name: 'ECDSA',
namedCurve: 'P-256', // Must match the curve used to generate the key
},
true, // whether the key is extractable (i.e., can be used in exportKey)
['sign'],
);

return async (data: string) => {
const encoder = new TextEncoder();
const signature = await subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-256' }, // Required for ES256
},
privateKey,
encoder.encode(data),
);

return btoa(String.fromCharCode(...new Uint8Array(signature)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, ''); // Convert to base64url format
};
},

async getVerifier(publicKeyJWK: object) {
const { subtle } = globalThis.crypto;
const publicKey = await subtle.importKey(
'jwk',
publicKeyJWK,
{
name: 'ECDSA',
namedCurve: 'P-256', // Must match the curve used to generate the key
},
true, // whether the key is extractable (i.e., can be used in exportKey)
['verify'],
);

return async (data: string, signatureBase64url: string) => {
const encoder = new TextEncoder();
const signature = Uint8Array.from(
atob(signatureBase64url.replace(/-/g, '+').replace(/_/g, '/')),
(c) => c.charCodeAt(0),
);
const isValid = await subtle.verify(
{
name: 'ECDSA',
hash: { name: 'SHA-256' }, // Required for ES256
},
publicKey,
signature,
encoder.encode(data),
);

return isValid;
};
},
};
27 changes: 26 additions & 1 deletion packages/node-crypto/src/test/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { describe, expect, test } from 'vitest';
import { generateSalt, digest } from '../crypto';
import { generateSalt, digest, ES256 } from '../index';

// Extract the major version as a number
const nodeVersionMajor = parseInt(
process.version.split('.')[0].substring(1),
10,
);

describe('This file is for utility functions', () => {
test('generateSalt', async () => {
Expand Down Expand Up @@ -27,4 +33,23 @@ describe('This file is for utility functions', () => {
expect(s1).toBeDefined();
expect(s1.length).toBe(64);
});

(nodeVersionMajor < 20 ? test.skip : test)('ES256', async () => {
const { privateKey, publicKey } = await ES256.generateKeyPair();
expect(privateKey).toBeDefined();
expect(publicKey).toBeDefined();
expect(typeof privateKey).toBe('object');
expect(typeof publicKey).toBe('object');

const data =
'In cryptography, a salt is random data that is used as an additional input to a one-way function that hashes data, a password or passphrase.';
const signer = await ES256.getSigner(privateKey);
const signature = await signer(data);
expect(signature).toBeDefined();
expect(typeof signature).toBe('string');

const verifier = await ES256.getVerifier(publicKey);
const result = await verifier(data, signature);
expect(result).toBe(true);
});
});

0 comments on commit 2d8206e

Please sign in to comment.