mirror of
https://github.com/cupcakearmy/occulto.git
synced 2025-12-08 10:55:01 +00:00
2.0.0 (#3)
* rewrite * testing worklflow * sprcify version * config stuff * add hash as buffer * delete docs * use typedarray everywhere and docs * readme * aes * cleanup * rsa * testing with playwright * fix playwright * readme * docs * update deps * use headless * add prepublish * build pipeline * move types up * move types up * add types legacy * packaging * versions bump * cleanup * maybe this time * drop support for commonjs * version bump * cleanup
This commit is contained in:
25
src/Hash.ts
25
src/Hash.ts
@@ -1,25 +0,0 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
|
||||
enum Hashes {
|
||||
MD5 = 'md5',
|
||||
SHA1_1 = 'sha1',
|
||||
SHA1_256 = 'sha256',
|
||||
SHA1_512 = 'sha512',
|
||||
SHA3_256 = 'sha3-256',
|
||||
SHA3_384 = 'sha3-384',
|
||||
SHA3_512 = 'sha3-512',
|
||||
}
|
||||
|
||||
export default class Hash {
|
||||
|
||||
static Hashes = Hashes
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} plain Input string
|
||||
* @param {Hashes} type Type of hash to be used to digest
|
||||
* @returns {string} Hash of the plain text
|
||||
*/
|
||||
static digest = (plain: string, type: Hashes = Hashes.SHA3_256): string => createHash(type).update(plain).digest().toString('hex')
|
||||
}
|
||||
50
src/RSA.ts
50
src/RSA.ts
@@ -1,50 +0,0 @@
|
||||
import { generateKeyPair, privateDecrypt, publicEncrypt } from 'crypto'
|
||||
|
||||
type PrivateKey = string
|
||||
type PublicKey = string
|
||||
|
||||
export type KeyPair = {
|
||||
pub: PublicKey
|
||||
prv: PrivateKey
|
||||
}
|
||||
|
||||
|
||||
export default class RSA {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} size Number of bits for the key
|
||||
* @returns {Promise<KeyPair>} Object that contains the key pair
|
||||
*/
|
||||
static gen = (size: number = 2 ** 12): Promise<KeyPair> => new Promise<KeyPair>((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
generateKeyPair('rsa', {
|
||||
modulusLength: size,
|
||||
publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
||||
}, (err: string, pub: string, prv: string) => {
|
||||
if (err) reject()
|
||||
else resolve({
|
||||
pub,
|
||||
prv,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} plain
|
||||
* @param {PublicKey} key
|
||||
* @returns {string} Encrypted string
|
||||
*/
|
||||
static encrypt = (plain: string, key: PublicKey): string => publicEncrypt(key, Buffer.from(plain)).toString('base64')
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} encrypted
|
||||
* @param {PrivateKey} key
|
||||
* @returns {string} Decrypted string
|
||||
*/
|
||||
static decrypt = (encrypted: string, key: PrivateKey): string => privateDecrypt(key, Buffer.from(encrypted, 'base64')).toString()
|
||||
|
||||
}
|
||||
162
src/Symmetric.ts
162
src/Symmetric.ts
@@ -1,162 +0,0 @@
|
||||
import {
|
||||
Cipher,
|
||||
CipherCCM,
|
||||
CipherGCM,
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
Decipher,
|
||||
DecipherCCM,
|
||||
DecipherGCM,
|
||||
randomBytes,
|
||||
scryptSync,
|
||||
} from 'crypto'
|
||||
import { TransformOptions } from 'stream'
|
||||
|
||||
import { Base64 } from './Util'
|
||||
|
||||
export enum Ciphers {
|
||||
ChaCha20,
|
||||
AES_256_GCM,
|
||||
AES_192_GCM,
|
||||
AES_128_GCM,
|
||||
AES_256_CTR,
|
||||
AES_192_CTR,
|
||||
AES_128_CTR,
|
||||
}
|
||||
|
||||
type CipherConfig = {
|
||||
alg: string,
|
||||
keySize: number,
|
||||
ivSize: number
|
||||
mac?: number
|
||||
}
|
||||
|
||||
type EncryptedItem = {
|
||||
alg: string,
|
||||
data: string,
|
||||
iv: string,
|
||||
salt: string
|
||||
keySize: number,
|
||||
tag?: string,
|
||||
tagSize?: number
|
||||
}
|
||||
|
||||
export default class Symmetric {
|
||||
|
||||
static Ciphers = Ciphers
|
||||
static Encoding: BufferEncoding = 'base64'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param plain {string} data The data to be encrypted
|
||||
* @param key {string} The encryption key
|
||||
* @param type {Ciphers} [type=Ciphers.AES_256_CTR] The cipher that will be used
|
||||
* @returns {string} Encrypted data as string
|
||||
*/
|
||||
static encrypt(plain: string, key: string, type: Ciphers = Ciphers.AES_256_CTR): string {
|
||||
const { alg, ivSize, mac, keySize } = Symmetric.getCipherConfig(type)
|
||||
|
||||
const iv = randomBytes(ivSize)
|
||||
const salt = randomBytes(keySize)
|
||||
const keyBuffered = scryptSync(Buffer.from(key), salt, keySize)
|
||||
|
||||
|
||||
const options: TransformOptions | undefined = mac ? { authTagLength: mac } as TransformOptions : undefined
|
||||
const cipher: CipherGCM | CipherCCM | Cipher = createCipheriv(alg, keyBuffered, iv, options)
|
||||
let content: Buffer = Buffer.concat([
|
||||
cipher.update(Buffer.from(plain)),
|
||||
cipher.final(),
|
||||
])
|
||||
|
||||
let tag: string | undefined = undefined
|
||||
// @ts-ignore
|
||||
if (mac) tag = cipher.getAuthTag().toString(Symmetric.Encoding)
|
||||
|
||||
const encrypted: EncryptedItem = {
|
||||
alg,
|
||||
data: content.toString(Symmetric.Encoding),
|
||||
tag,
|
||||
iv: iv.toString(Symmetric.Encoding),
|
||||
salt: salt.toString(Symmetric.Encoding),
|
||||
keySize,
|
||||
tagSize: mac,
|
||||
}
|
||||
|
||||
return Base64.encode(JSON.stringify(encrypted))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} encrypted The encrypted string
|
||||
* @param {string} key The key used for encrypting
|
||||
* @returns {string} The data as string
|
||||
*/
|
||||
static decrypt(encrypted: string, key: string): string {
|
||||
const { alg, data, iv, tag, salt, keySize, tagSize }: EncryptedItem = JSON.parse(Base64.decode(encrypted))
|
||||
const keyBuffered = scryptSync(Buffer.from(key), Buffer.from(salt, Symmetric.Encoding), keySize)
|
||||
|
||||
// @ts-ignore
|
||||
const options: TransformOptions = tag ? { authTagLength: tagSize } : undefined
|
||||
const decipher: DecipherGCM | DecipherCCM | Decipher = createDecipheriv(alg, keyBuffered, Buffer.from(iv, Symmetric.Encoding), options)
|
||||
|
||||
// @ts-ignore
|
||||
if (tag) decipher.setAuthTag(Buffer.from(tag, Symmetric.Encoding))
|
||||
|
||||
const decrypted: Buffer = Buffer.concat([
|
||||
decipher.update(Buffer.from(data, Symmetric.Encoding)),
|
||||
decipher.final(),
|
||||
])
|
||||
|
||||
return decrypted.toString()
|
||||
}
|
||||
|
||||
private static getCipherConfig = (type: Ciphers): CipherConfig => {
|
||||
switch (type) {
|
||||
case Ciphers.AES_128_GCM:
|
||||
return {
|
||||
alg: 'aes-128-gcm',
|
||||
ivSize: 16,
|
||||
keySize: 16,
|
||||
mac: 16,
|
||||
}
|
||||
case Ciphers.AES_192_GCM:
|
||||
return {
|
||||
alg: 'aes-192-gcm',
|
||||
ivSize: 16,
|
||||
keySize: 24,
|
||||
mac: 16,
|
||||
}
|
||||
case Ciphers.AES_256_GCM:
|
||||
return {
|
||||
alg: 'aes-256-gcm',
|
||||
ivSize: 16,
|
||||
keySize: 32,
|
||||
mac: 16,
|
||||
}
|
||||
case Ciphers.AES_128_CTR:
|
||||
return {
|
||||
alg: 'aes-128-ctr',
|
||||
ivSize: 16,
|
||||
keySize: 16,
|
||||
}
|
||||
case Ciphers.AES_192_CTR:
|
||||
return {
|
||||
alg: 'aes-192-ctr',
|
||||
ivSize: 16,
|
||||
keySize: 24,
|
||||
}
|
||||
case Ciphers.AES_256_CTR:
|
||||
return {
|
||||
alg: 'aes-256-ctr',
|
||||
ivSize: 16,
|
||||
keySize: 32,
|
||||
}
|
||||
case Ciphers.ChaCha20:
|
||||
return {
|
||||
alg: 'chacha20',
|
||||
ivSize: 16,
|
||||
keySize: 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Util.ts
25
src/Util.ts
@@ -1,25 +0,0 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
export class Base64 {
|
||||
static encode = (s: string): string => Buffer.from(s).toString('base64')
|
||||
static decode = (s: string): string => Buffer.from(s, 'base64').toString()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} length Length of the generated string
|
||||
* @param {boolean} string Whether to return a string or bytes
|
||||
* @returns {Buffer|String} The random string or buffer
|
||||
*/
|
||||
function Rand(length: number, string: false): Buffer
|
||||
function Rand(length: number, string: true): string
|
||||
function Rand(length: number, string: boolean = false): Buffer | string {
|
||||
const r = randomBytes(length)
|
||||
return string
|
||||
? r.toString('ascii')
|
||||
: r
|
||||
}
|
||||
|
||||
export default {
|
||||
Rand
|
||||
}
|
||||
144
src/crypto/aes.ts
Normal file
144
src/crypto/aes.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { type TypedArray } from '../utils/base.js'
|
||||
import { getCrypto } from './crypto.js'
|
||||
import { Base64, Bytes } from './encoding.js'
|
||||
import { Hashes } from './hash.js'
|
||||
import { getRandomBytes } from './random.js'
|
||||
|
||||
const Params = {
|
||||
GCM: {
|
||||
ivLength: 12,
|
||||
tagLength: 128,
|
||||
},
|
||||
}
|
||||
|
||||
export type KeyData = {
|
||||
name: 'PBKDF2'
|
||||
hash: Hashes
|
||||
iterations: number
|
||||
salt: TypedArray
|
||||
length: number
|
||||
}
|
||||
|
||||
/**
|
||||
* AES operation modes.
|
||||
*/
|
||||
export enum Modes {
|
||||
AES_GCM = 'AES-GCM',
|
||||
}
|
||||
|
||||
export class AES {
|
||||
static Modes = Modes
|
||||
|
||||
private static delimiter = '--' // delimiter with a character that is not allowed in base64 or hex
|
||||
private static delimiterEasy = '---'
|
||||
|
||||
private static InvalidCiphertext = new Error('Invalid ciphertext')
|
||||
|
||||
private static async join(...args: TypedArray[]): Promise<string> {
|
||||
const strings = await Promise.all(args.map(Base64.encode))
|
||||
return strings.join(AES.delimiter)
|
||||
}
|
||||
|
||||
private static async split(ciphertext: string): Promise<TypedArray[]> {
|
||||
const splitted = ciphertext.split(AES.delimiter)
|
||||
return Promise.all(splitted.map(Base64.decode))
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a key from a password.
|
||||
* To be used if the password is not 128, 192 or 256 bits or human made, non generated keys.
|
||||
*/
|
||||
static async derive(key: string, options?: KeyData): Promise<[TypedArray, KeyData]> {
|
||||
options ??= {
|
||||
name: 'PBKDF2',
|
||||
hash: Hashes.SHA_512,
|
||||
iterations: 100_000,
|
||||
length: 256,
|
||||
salt: await getRandomBytes(16),
|
||||
}
|
||||
const c = await getCrypto()
|
||||
const keyBuffer = await c.subtle.importKey('raw', Bytes.encode(key), options.name, false, [
|
||||
'deriveBits',
|
||||
'deriveKey',
|
||||
])
|
||||
const bits = await c.subtle.deriveBits(options, keyBuffer, options.length)
|
||||
return [new Uint8Array(bits), options]
|
||||
}
|
||||
|
||||
static async encrypt(data: TypedArray, key: TypedArray, mode: Modes = Modes.AES_GCM): Promise<string> {
|
||||
const c = await getCrypto()
|
||||
|
||||
let iv: Uint8Array
|
||||
let alg: AlgorithmIdentifier
|
||||
|
||||
switch (mode) {
|
||||
case Modes.AES_GCM:
|
||||
iv = c.getRandomValues(new Uint8Array(Params.GCM.ivLength))
|
||||
alg = mode
|
||||
break
|
||||
default:
|
||||
throw new Error('Unsupported mode')
|
||||
}
|
||||
|
||||
const keyObj = await c.subtle.importKey('raw', key, alg, false, ['encrypt'])
|
||||
const encrypted = await c.subtle.encrypt({ name: alg, iv }, keyObj, data)
|
||||
const encryptedBuffer = new Uint8Array(encrypted)
|
||||
|
||||
return AES.join(Bytes.encode(alg), iv, encryptedBuffer)
|
||||
}
|
||||
|
||||
static async decrypt(ciphertext: string, key: TypedArray): Promise<TypedArray> {
|
||||
const c = await getCrypto()
|
||||
|
||||
const [alg, iv, data] = await AES.split(ciphertext)
|
||||
if (!alg || !iv || !data) throw this.InvalidCiphertext
|
||||
|
||||
const mode = Bytes.decode(alg)
|
||||
switch (mode) {
|
||||
case Modes.AES_GCM:
|
||||
break
|
||||
default:
|
||||
throw new Error('Unsupported mode')
|
||||
}
|
||||
|
||||
const keyObj = await c.subtle.importKey('raw', key, mode, false, ['decrypt'])
|
||||
const decrypted = await c.subtle.decrypt({ name: mode, iv }, keyObj, data)
|
||||
return new Uint8Array(decrypted)
|
||||
}
|
||||
|
||||
static async encryptEasy(data: string | TypedArray, key: string, mode: Modes = Modes.AES_GCM): Promise<string> {
|
||||
const dataBuffer = typeof data === 'string' ? Bytes.encode(data) : data
|
||||
const [keyDerived, options] = await AES.derive(key)
|
||||
|
||||
const ciphertext = await this.encrypt(dataBuffer, keyDerived, mode)
|
||||
const header = await this.join(
|
||||
Bytes.encode(options.name),
|
||||
Bytes.encode(options.hash),
|
||||
Bytes.encode(options.iterations.toString()),
|
||||
options.salt,
|
||||
Bytes.encode(options.length.toString())
|
||||
)
|
||||
|
||||
return [header, ciphertext].join(this.delimiterEasy)
|
||||
}
|
||||
|
||||
static async decryptEasy(ciphertext: string, key: string): Promise<string> {
|
||||
const [header, data] = ciphertext.split(this.delimiterEasy)
|
||||
if (!header || !data) throw this.InvalidCiphertext
|
||||
const [name, hash, iterations, salt, length] = await this.split(header)
|
||||
if (!name || !hash || !iterations || !salt || !length) throw this.InvalidCiphertext
|
||||
|
||||
const options: KeyData = {
|
||||
name: Bytes.decode(name) as any,
|
||||
hash: Bytes.decode(hash) as any,
|
||||
iterations: parseInt(Bytes.decode(iterations)),
|
||||
salt,
|
||||
length: parseInt(Bytes.decode(length)),
|
||||
}
|
||||
if (isNaN(options.iterations) || isNaN(options.length)) throw this.InvalidCiphertext
|
||||
|
||||
const [keyDerived] = await AES.derive(key, options)
|
||||
const decrypted = await this.decrypt(data, keyDerived)
|
||||
return Bytes.decode(decrypted)
|
||||
}
|
||||
}
|
||||
20
src/crypto/crypto.ts
Normal file
20
src/crypto/crypto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { isBrowser } from '../utils/base.js'
|
||||
|
||||
let crypto: typeof window.crypto | null = null
|
||||
|
||||
export async function getCrypto(): Promise<typeof window.crypto> {
|
||||
if (!crypto) {
|
||||
if (isBrowser) crypto = window.crypto
|
||||
else if (typeof require !== 'undefined') {
|
||||
const { webcrypto } = await require('crypto')
|
||||
crypto = webcrypto
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const { webcrypto } = await import('crypto')
|
||||
crypto = webcrypto as any
|
||||
}
|
||||
}
|
||||
|
||||
if (!crypto) throw new Error('No crypto available')
|
||||
return crypto
|
||||
}
|
||||
83
src/crypto/encoding.ts
Normal file
83
src/crypto/encoding.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { split, type TypedArray } from '../utils/base.js'
|
||||
|
||||
export class Base64 {
|
||||
private static prefix = 'data:application/octet-stream;base64,'
|
||||
|
||||
static encode(s: TypedArray): Promise<string> {
|
||||
return split({
|
||||
async node() {
|
||||
return Buffer.from(s).toString('base64')
|
||||
},
|
||||
async browser() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (event) {
|
||||
const data = event.target?.result
|
||||
if (typeof data === 'string') resolve(data.slice(Base64.prefix.length))
|
||||
else reject(new Error('Failed to read file'))
|
||||
}
|
||||
reader.readAsDataURL(new Blob([s]))
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
static decode(s: string): Promise<TypedArray> {
|
||||
return split({
|
||||
async node() {
|
||||
return Buffer.from(s, 'base64')
|
||||
},
|
||||
async browser() {
|
||||
const ab = await fetch(Base64.prefix + s)
|
||||
.then((r) => r.blob())
|
||||
.then((b) => b.arrayBuffer())
|
||||
return new Uint8Array(ab)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Hex {
|
||||
static encode(buffer: TypedArray): string {
|
||||
let s = ''
|
||||
for (const i of new Uint8Array(buffer)) {
|
||||
s += i.toString(16).padStart(2, '0')
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
static decode(s: string): TypedArray {
|
||||
const size = s.length / 2
|
||||
const buffer = new Uint8Array(size)
|
||||
for (let i = 0; i < size; i++) {
|
||||
const idx = i * 2
|
||||
const segment = s.slice(idx, idx + 2)
|
||||
buffer[i] = parseInt(segment, 16)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
|
||||
export class Bytes {
|
||||
static decode(data: TypedArray): string {
|
||||
return split({
|
||||
node() {
|
||||
return Buffer.from(data).toString('utf-8')
|
||||
},
|
||||
browser() {
|
||||
return new TextDecoder().decode(data)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
static encode(data: string): TypedArray {
|
||||
return split({
|
||||
node() {
|
||||
return Buffer.from(data)
|
||||
},
|
||||
browser() {
|
||||
return new TextEncoder().encode(data)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
32
src/crypto/hash.ts
Normal file
32
src/crypto/hash.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type TypedArray } from '../utils/base.js'
|
||||
import { getCrypto } from './crypto.js'
|
||||
import { Bytes, Hex } from './encoding.js'
|
||||
|
||||
/**
|
||||
* List of available hash functions.
|
||||
*
|
||||
* @remarks
|
||||
* For cryptographic details refer to: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#supported_algorithms
|
||||
* Reference: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
|
||||
*/
|
||||
export enum Hashes {
|
||||
/**
|
||||
* @remarks SHA-1 is not recommended for new applications as it's not cryptographically secure.
|
||||
*/
|
||||
SHA_1 = 'SHA-1',
|
||||
SHA_256 = 'SHA-256',
|
||||
SHA_384 = 'SHA-384',
|
||||
SHA_512 = 'SHA-512',
|
||||
}
|
||||
|
||||
export class Hash {
|
||||
static async hash(data: string, hash: Hashes): Promise<string>
|
||||
static async hash(data: TypedArray, hash: Hashes): Promise<TypedArray>
|
||||
static async hash(data: string | TypedArray, hash: Hashes): Promise<string | TypedArray> {
|
||||
const isString = typeof data === 'string'
|
||||
const c = await getCrypto()
|
||||
const result = await c.subtle.digest(hash, isString ? Bytes.encode(data) : data)
|
||||
const buf = new Uint8Array(result)
|
||||
return isString ? Hex.encode(buf) : buf
|
||||
}
|
||||
}
|
||||
11
src/crypto/random.ts
Normal file
11
src/crypto/random.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { type TypedArray } from '../utils/base.js'
|
||||
import { getCrypto } from './crypto.js'
|
||||
|
||||
export async function getRandomBytes(bytes: number): Promise<TypedArray> {
|
||||
if (bytes <= 0) throw new Error('Invalid number of bytes')
|
||||
|
||||
const buffer = new Uint8Array(bytes)
|
||||
const crypto = await getCrypto()
|
||||
crypto.getRandomValues(buffer)
|
||||
return buffer
|
||||
}
|
||||
137
src/crypto/rsa.ts
Normal file
137
src/crypto/rsa.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { TypedArray } from '../utils/base.js'
|
||||
import { getCrypto } from './crypto.js'
|
||||
import { Base64 } from './encoding.js'
|
||||
|
||||
const Constants = Object.freeze({
|
||||
name: 'RSA-OAEP',
|
||||
hash: 'SHA-512',
|
||||
exponent: new Uint8Array([1, 0, 1]),
|
||||
error: {
|
||||
invalidKey: new Error('invalid key'),
|
||||
shouldBePublicKey: new Error('should be a public key'),
|
||||
shouldBePrivateKey: new Error('should be a private key'),
|
||||
dataTooLong: new Error('data too long'),
|
||||
},
|
||||
})
|
||||
|
||||
class Key {
|
||||
/**
|
||||
* Exports a key to a PEM string.
|
||||
*/
|
||||
static async encode(key: CryptoKey): Promise<string> {
|
||||
const c = await getCrypto()
|
||||
|
||||
let type: 'pkcs8' | 'spki'
|
||||
let label: string
|
||||
switch (key.type) {
|
||||
case 'private':
|
||||
type = 'pkcs8'
|
||||
label = 'PRIVATE'
|
||||
break
|
||||
case 'public':
|
||||
type = 'spki'
|
||||
label = 'PUBLIC'
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error('invalid key type')
|
||||
}
|
||||
const exported = await c.subtle.exportKey(type, key)
|
||||
|
||||
// Export in PEM format
|
||||
const base64 = await Base64.encode(new Uint8Array(exported))
|
||||
const formatted = base64.match(/.{1,64}/g)?.join('\n')
|
||||
return `-----BEGIN ${label} KEY-----\n${formatted}\n-----END ${label} KEY-----`
|
||||
}
|
||||
|
||||
static async decode(s: string): Promise<CryptoKey> {
|
||||
const isPrivate = s.includes('BEGIN PRIVATE KEY')
|
||||
const cleaned = s.replace(/-----.*?-----/g, '').replace(/\s/g, '')
|
||||
const bytes = await Base64.decode(cleaned)
|
||||
const c = await getCrypto()
|
||||
return await c.subtle.importKey(
|
||||
isPrivate ? 'pkcs8' : 'spki',
|
||||
bytes,
|
||||
{
|
||||
name: Constants.name,
|
||||
hash: Constants.hash,
|
||||
},
|
||||
true,
|
||||
isPrivate ? ['decrypt'] : ['encrypt']
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max size of payload to be encrypted.
|
||||
* The size depends on the key size and the hash function used.
|
||||
* https://www.rfc-editor.org/rfc/rfc2437#section-7.1.1
|
||||
*/
|
||||
static getMaxMessageSize(key: CryptoKey): number {
|
||||
if (key.type !== 'public') throw Constants.error.shouldBePublicKey
|
||||
// @ts-ignore
|
||||
const mod = key?.algorithm?.modulusLength
|
||||
if (isNaN(mod)) throw Constants.error.invalidKey
|
||||
return mod / 8 - (2 * 512) / 8 - 2
|
||||
}
|
||||
}
|
||||
|
||||
export class RSA {
|
||||
static async generateKeyPair(bits: number = 2 ** 12): Promise<{ private: string; public: string }> {
|
||||
const c = await getCrypto()
|
||||
|
||||
if (bits < 2 ** 11) {
|
||||
throw new Error('bit sizes below 2048 are considered insecure.')
|
||||
}
|
||||
|
||||
const pair = await c.subtle.generateKey(
|
||||
{
|
||||
name: Constants.name,
|
||||
modulusLength: bits,
|
||||
publicExponent: Constants.exponent,
|
||||
hash: Constants.hash,
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
return {
|
||||
private: await Key.encode(pair.privateKey),
|
||||
public: await Key.encode(pair.publicKey),
|
||||
}
|
||||
}
|
||||
|
||||
static async encrypt(data: TypedArray, key: string): Promise<TypedArray> {
|
||||
let keyObj: CryptoKey
|
||||
try {
|
||||
keyObj = await Key.decode(key)
|
||||
} catch (e) {
|
||||
throw Constants.error.invalidKey
|
||||
}
|
||||
if (keyObj.type !== 'public') {
|
||||
throw Constants.error.shouldBePublicKey
|
||||
}
|
||||
|
||||
// Check if data is too large
|
||||
if (data.length > Key.getMaxMessageSize(keyObj)) throw Constants.error.dataTooLong
|
||||
|
||||
const c = await getCrypto()
|
||||
const encrypted = await c.subtle.encrypt({ name: Constants.name }, keyObj, data)
|
||||
return new Uint8Array(encrypted)
|
||||
}
|
||||
|
||||
static async decrypt(data: TypedArray, key: string): Promise<TypedArray> {
|
||||
let keyObj: CryptoKey
|
||||
try {
|
||||
keyObj = await Key.decode(key)
|
||||
} catch (e) {
|
||||
throw Constants.error.invalidKey
|
||||
}
|
||||
if (keyObj.type !== 'private') {
|
||||
throw Constants.error.shouldBePrivateKey
|
||||
}
|
||||
|
||||
const c = await getCrypto()
|
||||
const decrypted = await c.subtle.decrypt({ name: Constants.name }, keyObj, data)
|
||||
return new Uint8Array(decrypted)
|
||||
}
|
||||
}
|
||||
26
src/index.ts
26
src/index.ts
@@ -1,20 +1,6 @@
|
||||
import H from './Hash'
|
||||
import R from './RSA'
|
||||
import S from './Symmetric'
|
||||
import U from './Util'
|
||||
|
||||
export const RSA = R
|
||||
export const Symmetric = S
|
||||
export const Hash = H
|
||||
export const Util = U
|
||||
|
||||
|
||||
export default {
|
||||
RSA,
|
||||
Symmetric,
|
||||
Hash,
|
||||
Util,
|
||||
}
|
||||
|
||||
// Require node 11
|
||||
if (parseInt(process.versions.node.split('.')[0]) < 11) throw new Error('Node 11 or higher is required')
|
||||
export * from './crypto/aes.js'
|
||||
export * from './crypto/encoding.js'
|
||||
export * from './crypto/hash.js'
|
||||
export * from './crypto/random.js'
|
||||
export * from './crypto/rsa.js'
|
||||
export * from './utils/base.js'
|
||||
|
||||
27
src/utils/base.ts
Normal file
27
src/utils/base.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type PromiseOrValue<T> = T | Promise<T>
|
||||
|
||||
export type TypedArray =
|
||||
| Int8Array
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Int16Array
|
||||
| Uint16Array
|
||||
| Int32Array
|
||||
| Uint32Array
|
||||
| BigInt64Array
|
||||
| BigUint64Array
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function split<T>({ node, browser }: { node: () => T; browser: () => T }) {
|
||||
return isBrowser ? browser() : node()
|
||||
}
|
||||
Reference in New Issue
Block a user