Skip to content
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: 3 additions & 1 deletion packages/signers/massa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
"dependencies": {
"@enkryptcom/types": "workspace:^",
"@enkryptcom/utils": "workspace:^",
"@massalabs/massa-web3": "^5.2.1-dev",
"@noble/ed25519": "^2.3.0",
"@noble/hashes": "^1.8.0",
"bip39": "^3.1.0",
"bs58check": "^4.0.0",
"tweetnacl": "^1.0.3",
"varint": "^6.0.0"
},
Expand Down
17 changes: 17 additions & 0 deletions packages/signers/massa/src/crypto/base58.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import bs58check from 'bs58check'
import Serializer from './interfaces/serializer'

/**
* Base58 implementation of the Serializer interface.
*/
export default class Base58 implements Serializer {
// eslint-disable-next-line class-methods-use-this -- Expected by the interface.
serialize(data: Uint8Array): string {
return bs58check.encode(data)
}

// eslint-disable-next-line class-methods-use-this -- Expected by the interface.
deserialize(data: string): Uint8Array {
return bs58check.decode(data)
}
}
12 changes: 12 additions & 0 deletions packages/signers/massa/src/crypto/blake3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { blake3 as hashBlake3 } from '@noble/hashes/blake3'
import Hasher from './interfaces/hasher'

/**
* Blake3 implementation of the Hasher interface.
*/
export default class Blake3 implements Hasher {
// eslint-disable-next-line class-methods-use-this -- Expected by the interface.
hash(data: Uint8Array | string): Uint8Array {
return hashBlake3(data)
}
}
217 changes: 217 additions & 0 deletions packages/signers/massa/src/crypto/cross-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/* This module contains cryptographic functions federating between Node.js and the browser.
* @packageDocumentation
*
* @privateRemarks
* If you extend this module, please check that the functions are working in both Node.js and the browser.
*/

const KEY_SIZE_BYTES = 32
const IV_SIZE_BYTES = 12
const AUTH_TAG_SIZE_BYTES = 16

const U8_SIZE_BITS = 8

function isNode(): boolean {
// inspired from secure-random.js
// we check for process.pid to prevent browserify from tricking us
return (
typeof process !== 'undefined' &&
typeof process.pid === 'number' &&
typeof process.versions?.node === 'string'
)
}

async function pbkdf2Node(
password: string,
salt: Buffer,
opts: PBKDF2Options
): Promise<Uint8Array> {
const { iterations, keyLength, hash } = opts
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto')
return new Promise((resolve, reject) => {
crypto.pbkdf2(
password,
salt,
iterations,
keyLength,
hash,
(err, derivedKey) => {
if (err) reject(err)
else resolve(derivedKey)
}
)
})
}

async function pbkdf2Browser(
password: string,
salt: Buffer,
opts: PBKDF2Options
): Promise<Uint8Array> {
const { iterations, keyLength, hash } = opts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const crypto = window.crypto || (window as any).msCrypto

if (!crypto) throw new Error('Your browser does not expose window.crypto.')

const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
)
const derivedKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: iterations,
hash: { name: hash },
},
keyMaterial,
{ name: 'AES-GCM', length: keyLength * U8_SIZE_BITS },
false,
['encrypt', 'decrypt']
)

const exportedKey = await crypto.subtle.exportKey('raw', derivedKey)
const buffer = Buffer.from(exportedKey)

return buffer
}

export type PBKDF2Options = {
iterations: number
keyLength: number
hash: string
}

/**
* Derives a cryptographic key using PBKDF2.
*
* @param password - The password from which to derive the key.
* @param salt - The cryptographic salt.
* @param opts - Options for the derivation.
*
* @returns The derived key.
*/
export async function pbkdf2(
password: string,
salt: Buffer,
opts: PBKDF2Options
): Promise<Uint8Array> {
if (isNode()) {
return pbkdf2Node(password, salt, opts)
}

return pbkdf2Browser(password, salt, opts)
}

/**
* Seals data using AES-256-GCM encryption.
*
* @param data - The data to encrypt.
* @param key - The 32-byte secret key.
* @param iv - The 12-byte initialization vector.
*
* @throws If the key is not 32 bytes.
* @throws If the iv is not 12 bytes.
*
* @returns The encrypted data.
*/
export async function aesGCMEncrypt(
data: Uint8Array,
key: Uint8Array,
iv: Uint8Array
): Promise<Uint8Array> {
if (key.length !== KEY_SIZE_BYTES) {
throw new Error(`key must be ${KEY_SIZE_BYTES} bytes`)
}
if (iv.length !== IV_SIZE_BYTES) {
throw new Error(`iv must be ${IV_SIZE_BYTES} bytes`)
}
if (isNode()) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto')
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const encrypted = Buffer.concat([
cipher.update(data),
cipher.final(),
cipher.getAuthTag(),
])
return encrypted
}

const keyData = await window.crypto.subtle.importKey(
'raw',
key,
{ name: 'AES-GCM' },
false,
['encrypt']
)
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
keyData,
data
)
return Buffer.from(encrypted)
}

/**
* Unseals data using AES-256-GCM decryption.
*
* @remarks
* The authentication tag shall be appended to the encryptedData.
*
* @param encryptedData - The data to decrypt.
* @param key - The 32-byte secret key.
* @param iv - The 12-byte initialization vector.
*
* @throws If the key is not 32 bytes.
* @throws If the iv is not 12 bytes.
*
* @returns The decrypted data.
*/
export async function aesGCMDecrypt(
encryptedData: Uint8Array,
key: Uint8Array,
iv: Uint8Array
): Promise<Uint8Array> {
if (key.length !== KEY_SIZE_BYTES) {
throw new Error(`key must be ${KEY_SIZE_BYTES} bytes`)
}
if (iv.length !== IV_SIZE_BYTES) {
throw new Error(`iv must be ${IV_SIZE_BYTES} bytes`)
}
if (isNode()) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto')
encryptedData = Buffer.from(encryptedData)
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
decipher.setAuthTag(
encryptedData.slice(encryptedData.length - AUTH_TAG_SIZE_BYTES)
)
const decrypted = Buffer.concat([
decipher.update(
encryptedData.slice(0, encryptedData.length - AUTH_TAG_SIZE_BYTES)
),
decipher.final(),
])
return decrypted
}

const keyData = await window.crypto.subtle.importKey(
'raw',
key,
{ name: 'AES-GCM' },
false,
['decrypt']
)
const decrypted = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
keyData,
encryptedData
)
return Buffer.from(decrypted)
}
33 changes: 33 additions & 0 deletions packages/signers/massa/src/crypto/ed25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha2";
ed.etc.sha512Sync = sha512; // Use the sha512 implementation from noble
import Signer from "./interfaces/signer";

/**
* Ed25519 implementation of the Signer interface.
*/
export default class Ed25519 implements Signer {
// eslint-disable-next-line class-methods-use-this -- Expected by the interface.
generatePrivateKey(): Uint8Array {
return ed.utils.randomPrivateKey();
}

// eslint-disable-next-line class-methods-use-this -- Expected by the interface.
async getPublicKey(privateKey: Uint8Array): Promise<Uint8Array> {
return ed.getPublicKey(privateKey);
}

// eslint-disable-next-line class-methods-use-this -- Expected by the interface.
async sign(privateKey: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
return ed.sign(data, privateKey);
}

// eslint-disable-next-line class-methods-use-this -- Expected by the interface.
async verify(
publicKey: Uint8Array,
data: Uint8Array,
signature: Uint8Array,
): Promise<boolean> {
return ed.verify(signature, data, publicKey);
}
}
4 changes: 4 additions & 0 deletions packages/signers/massa/src/crypto/interfaces/hasher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- True interface.
export default interface Hasher {
hash(data: Buffer | Uint8Array | string): Uint8Array
}
7 changes: 7 additions & 0 deletions packages/signers/massa/src/crypto/interfaces/sealer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- True interface.
export default interface Sealer {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
seal(data: Uint8Array): Promise<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unseal(data: any): Promise<Uint8Array>
}
5 changes: 5 additions & 0 deletions packages/signers/massa/src/crypto/interfaces/serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- True interface.
export default interface Serializer {
serialize(data: Uint8Array): string
deserialize(data: string): Uint8Array
}
11 changes: 11 additions & 0 deletions packages/signers/massa/src/crypto/interfaces/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- True interface.
export default interface Signer {
generatePrivateKey(): Uint8Array
getPublicKey(privateKey: Uint8Array): Promise<Uint8Array>
sign(privateKey: Uint8Array, data: Uint8Array): Promise<Uint8Array>
verify(
publicKey: Uint8Array,
data: Uint8Array,
signature: Uint8Array
): Promise<boolean>
}
10 changes: 10 additions & 0 deletions packages/signers/massa/src/crypto/interfaces/versioner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- True interface.
export interface Versioner {
attach(version: Version, data: Uint8Array): Uint8Array
extract(data: Uint8Array): { version: Version; data: Uint8Array }
}

export enum Version {
V0 = 0,
V1 = 1,
}
Loading
Loading