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

feat(crypto): add crypto utils #9

Merged
merged 4 commits into from
Aug 14, 2024
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
4 changes: 4 additions & 0 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"path": "packages/cookie-storage/dist/index.js",
"limit": "990 B"
},
{
"path": "packages/crypto/dist/index.js",
"limit": "27.3 KB"
},
{
"path": "packages/debug/dist/index.js",
"limit": "3.4 KB"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Common utils used by Rambler team

- [@rambler-tech/async](packages/async)
- [@rambler-tech/cookie-storage](packages/cookie-storage)
- [@rambler-tech/crypto](packages/crypto)
- [@rambler-tech/debug](packages/debug)
- [@rambler-tech/dom](packages/dom)
- [@rambler-tech/lhci-report](packages/lhci-report)
Expand Down
15 changes: 15 additions & 0 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Crypto

Browser crypto utils

## Install

```
npm install -D @rambler-tech/crypto
```

or

```
yarn add -D @rambler-tech/crypto
```
72 changes: 72 additions & 0 deletions packages/crypto/aesrsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {getRandomValues, subtle} from './crypto'
import {
bufferFromString,
bufferFromUnicode,
stringFromBuffer,
base64urlFromString
} from './buffers'

function generateAESKey() {
return subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true,
['encrypt', 'decrypt']
)
}

async function encryptAES(
key: CryptoKey,
initVector: Uint8Array,
body: string
) {
const encryptedBody = await subtle.encrypt(
{name: 'AES-GCM', iv: initVector},
key,
bufferFromUnicode(body)
)

return base64urlFromString(stringFromBuffer(encryptedBody))
}

function importRSAKey(keyString: string) {
return subtle.importKey(
'spki',
bufferFromString(window.atob(keyString)),
{
name: 'RSA-OAEP',
hash: {name: 'SHA-256'}
},
false,
['wrapKey']
)
}

async function encryptRSA(key: CryptoKey, body: CryptoKey) {
const encryptedBody = await subtle.wrapKey('raw', body, key, {
name: 'RSA-OAEP'
} as RsaOaepParams)

return base64urlFromString(stringFromBuffer(encryptedBody))
}

/** Encrypt with AES and RSA keys */
export async function encryptAESRSA(keyString: string, body: string) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const initVector = getRandomValues(new Uint8Array(12))
const initVectorString = base64urlFromString(stringFromBuffer(initVector))

const [aesKey, rsaKey] = await Promise.all([
generateAESKey(),
importRSAKey(keyString)
])

const [encryptedBody, encryptedKey] = await Promise.all([
encryptAES(aesKey, initVector, body),
encryptRSA(rsaKey, aesKey)
])

return `${encryptedBody}.${encryptedKey}.${initVectorString}`
}
32 changes: 32 additions & 0 deletions packages/crypto/bcrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import sha from 'sha.js'
import {hash} from './bcryptjs'

/** bcrypt derivation params */
export interface DeriveBcryptKeyParams {
salt: string
token: string
}

/** Derive key with bcrypt */
export async function deriveBcryptKey(
input: string,
{salt, token}: DeriveBcryptKeyParams
) {
const hashString: string = await new Promise((resolve, reject) => {
hash(input, salt, (error, hash) => {
if (error) {
reject(error)
} else {
resolve(hash)
}
})
})

if (!token) {
return hashString
}

return sha('sha512')
.update(hashString + token)
.digest('hex')
}
142 changes: 142 additions & 0 deletions packages/crypto/bcryptjs.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Type definitions for bcryptjs v2.4.0
// Project: https://github.com/dcodeIO/bcrypt.js
// Definitions by: Joshua Filby <https://github.com/Joshua-F/>
// Rafael Kraut <https://github.com/RafaelKr>
// Branislav Holý <https://github.com/branoholy>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.1

/**
* Sets the pseudo random number generator to use as a fallback if neither node's crypto module nor the Web Crypto API is available.
* Please note: It is highly important that the PRNG used is cryptographically secure and that it is seeded properly!
* @param random Function taking the number of bytes to generate as its sole argument, returning the corresponding array of cryptographically secure random byte values.
*/
export declare function setRandomFallback(
random: (random: number) => Buffer
): void

/**
* Synchronously generates a salt.
* @param rounds Number of rounds to use, defaults to 10 if omitted
* @return Resulting salt
* @throws If a random fallback is required but not set
*/
export declare function genSaltSync(rounds?: number): string

/**
* Asynchronously generates a salt.
* @param rounds Number of rounds to use, defaults to 10 if omitted
* @return Promise with resulting salt, if callback has been omitted
*/
export declare function genSalt(rounds?: number): Promise<string>

/**
* Asynchronously generates a salt.
* @param callback Callback receiving the error, if any, and the resulting salt
*/
export declare function genSalt(
callback: (err: Error, salt: string) => void
): void

/**
* Asynchronously generates a salt.
* @param rounds Number of rounds to use, defaults to 10 if omitted
* @param callback Callback receiving the error, if any, and the resulting salt
*/
export declare function genSalt(
rounds: number,
callback: (err: Error, salt: string) => void
): void

/**
* Synchronously generates a hash for the given string.
* @param s String to hash
* @param salt Salt length to generate or salt to use, default to 10
* @return Resulting hash
*/
export declare function hashSync(s: string, salt?: number | string): string

/**
* Asynchronously generates a hash for the given string.
* @param s String to hash
* @param salt Salt length to generate or salt to use
* @return Promise with resulting hash, if callback has been omitted
*/
export declare function hash(s: string, salt: number | string): Promise<string>

/**
* Asynchronously generates a hash for the given string.
* @param s String to hash
* @param salt Salt length to generate or salt to use
* @param callback Callback receiving the error, if any, and the resulting hash
* @param progressCallback Callback successively called with the percentage of rounds completed (0.0 - 1.0), maximally once per MAX_EXECUTION_TIME = 100 ms.
*/
export declare function hash(
s: string,
salt: number | string,
callback?: (err: Error, hash: string) => void,
progressCallback?: (percent: number) => void
): void

/**
* Synchronously tests a string against a hash.
* @param s String to compare
* @param hash Hash to test against
* @return true if matching, otherwise false
*/
export declare function compareSync(s: string, hash: string): boolean

/**
* Asynchronously compares the given data against the given hash.
* @param s Data to compare
* @param hash Data to be compared to
* @return Promise, if callback has been omitted
*/
export declare function compare(s: string, hash: string): Promise<boolean>

/**
* Asynchronously compares the given data against the given hash.
* @param s Data to compare
* @param hash Data to be compared to
* @param callback Callback receiving the error, if any, otherwise the result
* @param progressCallback Callback successively called with the percentage of rounds completed (0.0 - 1.0), maximally once per MAX_EXECUTION_TIME = 100 ms.
*/
export declare function compare(
s: string,
hash: string,
callback?: (err: Error, success: boolean) => void,
progressCallback?: (percent: number) => void
): void

/**
* Gets the number of rounds used to encrypt the specified hash.
* @param hash Hash to extract the used number of rounds from
* @return Number of rounds used
*/
export declare function getRounds(hash: string): number

/**
* Gets the salt portion from a hash. Does not validate the hash.
* @param hash Hash to extract the salt from
* @return Extracted salt part
*/
export declare function getSalt(hash: string): string

/**
* Encodes a byte array to base64 with up to len bytes of input, using the custom bcrypt alphabet.
* @function
* @param b Byte array
* @param len Maximum input length
*/
export declare function encodeBase64(
b: Readonly<ArrayLike<number>>,
len: number
): string

/**
* Decodes a base64 encoded string to up to len bytes of output, using the custom bcrypt alphabet.
* @function
* @param s String to decode
* @param len Maximum output length
*/
export declare function decodeBase64(s: string, len: number): number[]
Loading
Loading