Skip to content

Commit

Permalink
refactor: type check crypto functions (#1144)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf authored Jul 27, 2023
1 parent 51d13e3 commit c0085eb
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .changeset/tricky-ads-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
35 changes: 21 additions & 14 deletions packages/crypto/src/browser/aes-ctr.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { arrayify } from '@ethersproject/bytes';
import { pbkdf2 } from '@ethersproject/pbkdf2';

import type { Keystore } from '../types';
import type { CryptoApi, Keystore } from '../types';

import { btoa } from './crypto';
import { randomBytes } from './randomBytes';

const ALGORITHM = 'AES-CTR';

export function bufferFromString(
export const bufferFromString: CryptoApi['bufferFromString'] = (
string: string,
encoding: 'utf-8' | 'base64' = 'base64'
): Uint8Array {
): Uint8Array => {
if (encoding === 'utf-8') {
return new TextEncoder().encode(string);
}
Expand All @@ -21,32 +21,36 @@ export function bufferFromString(
.split('')
.map((c) => c.charCodeAt(0))
);
}
};

export function stringFromBuffer(
export const stringFromBuffer: CryptoApi['stringFromBuffer'] = (
buffer: Uint8Array,
_encoding: 'utf-8' | 'base64' = 'base64'
): string {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer) as unknown as number[]));
}
): string => btoa(String.fromCharCode.apply(null, new Uint8Array(buffer) as unknown as number[]));

/**
* Generate a pbkdf2 key from a password and random salt
*/
export function keyFromPassword(password: string, saltBuffer: Uint8Array): Uint8Array {
export const keyFromPassword: CryptoApi['keyFromPassword'] = (
password: string,
saltBuffer: Uint8Array
): Uint8Array => {
const passBuffer = bufferFromString(String(password).normalize('NFKC'), 'utf-8');
const key = pbkdf2(passBuffer, saltBuffer, 100000, 32, 'sha256');

return arrayify(key);
}
};

/**
* Encrypts a data object that can be any serializable value using
* a provided password.
*
* @returns Promise<Keystore> object
*/
export async function encrypt<T>(password: string, data: T): Promise<Keystore> {
export const encrypt: CryptoApi['encrypt'] = async <T>(
password: string,
data: T
): Promise<Keystore> => {
const iv = randomBytes(16);
const salt = randomBytes(32);
const secret = keyFromPassword(password, salt);
Expand All @@ -65,13 +69,16 @@ export async function encrypt<T>(password: string, data: T): Promise<Keystore> {
iv: stringFromBuffer(iv),
salt: stringFromBuffer(salt),
};
}
};

/**
* Given a password and a keystore object, decrypts the text and returns
* the resulting value
*/
export async function decrypt<T>(password: string, keystore: Keystore): Promise<T> {
export const decrypt: CryptoApi['decrypt'] = async <T>(
password: string,
keystore: Keystore
): Promise<T> => {
const iv = bufferFromString(keystore.iv);
const salt = bufferFromString(keystore.salt);
const secret = keyFromPassword(password, salt);
Expand All @@ -92,4 +99,4 @@ export async function decrypt<T>(password: string, keystore: Keystore): Promise<
} catch {
throw new Error('Invalid credentials');
}
}
};
15 changes: 15 additions & 0 deletions packages/crypto/src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { CryptoApi } from '../types';

import { bufferFromString, stringFromBuffer, decrypt, encrypt, keyFromPassword } from './aes-ctr';
import { randomBytes } from './randomBytes';

const api: CryptoApi = {
bufferFromString,
stringFromBuffer,
decrypt,
encrypt,
keyFromPassword,
randomBytes,
};

export default api;
4 changes: 3 additions & 1 deletion packages/crypto/src/browser/randomBytes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { CryptoApi } from '../types';

import { crypto } from './crypto';

export const randomBytes = (length: number) => {
export const randomBytes: CryptoApi['randomBytes'] = (length: number): Uint8Array => {
const randomValues = crypto.getRandomValues(new Uint8Array(length));
return randomValues;
};
13 changes: 11 additions & 2 deletions packages/crypto/src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export * from './browser/aes-ctr';
export * from './browser/randomBytes';
import cryptoApi from './browser';

export * from './types';

export const {
bufferFromString,
decrypt,
encrypt,
keyFromPassword,
randomBytes,
stringFromBuffer,
} = cryptoApi;
13 changes: 11 additions & 2 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export * from './node/aes-ctr';
export * from './node/randomBytes';
import cryptoApi from './node';

export * from './types';

export const {
bufferFromString,
decrypt,
encrypt,
keyFromPassword,
randomBytes,
stringFromBuffer,
} = cryptoApi;
35 changes: 20 additions & 15 deletions packages/crypto/src/node/aes-ctr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,45 @@ import { arrayify } from '@ethersproject/bytes';
import { pbkdf2 } from '@ethersproject/pbkdf2';
import crypto from 'crypto';

import type { Keystore } from '../types';
import type { CryptoApi, Keystore } from '../types';

import { randomBytes } from './randomBytes';

const ALGORITHM = 'aes-256-ctr';

export function bufferFromString(
export const bufferFromString: CryptoApi['bufferFromString'] = (
string: string,
encoding: 'utf-8' | 'base64' = 'base64'
): Uint8Array {
return Buffer.from(string, encoding);
}
): Uint8Array => Buffer.from(string, encoding);

export function stringFromBuffer(
export const stringFromBuffer: CryptoApi['stringFromBuffer'] = (
buffer: Uint8Array,
encoding: 'utf-8' | 'base64' = 'base64'
): string {
return Buffer.from(buffer).toString(encoding);
}
): string => Buffer.from(buffer).toString(encoding);

/**
* Generate a pbkdf2 key from a password and random salt
*/
export function keyFromPassword(password: string, saltBuffer: Uint8Array): Uint8Array {
export const keyFromPassword: CryptoApi['keyFromPassword'] = (
password: string,
saltBuffer: Uint8Array
): Uint8Array => {
const passBuffer = bufferFromString(String(password).normalize('NFKC'), 'utf-8');
const key = pbkdf2(passBuffer, saltBuffer, 100000, 32, 'sha256');

return arrayify(key);
}
};

/**
* Encrypts a data object that can be any serializable value using
* a provided password.
*
* @returns Promise<Keystore> object
*/
export async function encrypt<T>(password: string, data: T): Promise<Keystore> {
export const encrypt: CryptoApi['encrypt'] = async <T>(
password: string,
data: T
): Promise<Keystore> => {
const iv = randomBytes(16);
const salt = randomBytes(32);
const secret = keyFromPassword(password, salt);
Expand All @@ -53,13 +55,16 @@ export async function encrypt<T>(password: string, data: T): Promise<Keystore> {
iv: stringFromBuffer(iv),
salt: stringFromBuffer(salt),
};
}
};

/**
* Given a password and a keystore object, decrypts the text and returns
* the resulting value
*/
export async function decrypt<T>(password: string, keystore: Keystore): Promise<T> {
export const decrypt: CryptoApi['decrypt'] = async <T>(
password: string,
keystore: Keystore
): Promise<T> => {
const iv = bufferFromString(keystore.iv);
const salt = bufferFromString(keystore.salt);
const secret = keyFromPassword(password, salt);
Expand All @@ -75,4 +80,4 @@ export async function decrypt<T>(password: string, keystore: Keystore): Promise<
} catch {
throw new Error('Invalid credentials');
}
}
};
15 changes: 15 additions & 0 deletions packages/crypto/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { CryptoApi } from '../types';

import { bufferFromString, stringFromBuffer, decrypt, encrypt, keyFromPassword } from './aes-ctr';
import { randomBytes } from './randomBytes';

const api: CryptoApi = {
bufferFromString,
stringFromBuffer,
decrypt,
encrypt,
keyFromPassword,
randomBytes,
};

export default api;
6 changes: 4 additions & 2 deletions packages/crypto/src/node/randomBytes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import crypto from 'crypto';

export const randomBytes = (length: number) => {
const randomValues = crypto.randomBytes(length);
import type { CryptoApi } from '../types';

export const randomBytes: CryptoApi['randomBytes'] = (length: number): Uint8Array => {
const randomValues = Uint8Array.from(crypto.randomBytes(length));
return randomValues;
};
9 changes: 9 additions & 0 deletions packages/crypto/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@ export interface Keystore {
iv: string;
salt: string;
}

export interface CryptoApi {
bufferFromString(string: string, encoding?: 'utf-8' | 'base64'): Uint8Array;
decrypt<T>(password: string, keystore: Keystore): Promise<T>;
encrypt<T>(password: string, data: T): Promise<Keystore>;
keyFromPassword(password: string, saltBuffer: Uint8Array): Uint8Array;
stringFromBuffer(buffer: Uint8Array, encoding?: 'utf-8' | 'base64'): string;
randomBytes(length: number): Uint8Array;
}

0 comments on commit c0085eb

Please sign in to comment.