diff --git a/building-blocks/README.md b/building-blocks/README.md index f7624eaa..72e6ee38 100644 --- a/building-blocks/README.md +++ b/building-blocks/README.md @@ -55,7 +55,18 @@ Path: [`./read-data-feeds`](./read-data-feeds) - On a cron schedule, reads `decimals()` and `latestAnswer()` from **Chainlink Data Feeds** (example targets BTC/USD and ETH/USD on Arbitrum One). - Shows how to add an ABI, generate Go bindings, configure chain RPC, and log scaled answers. -👉 See the block’s README for setup, config, and sample logs. +👉 See the block's README for setup, config, and sample logs. + +--- + +### 3) **Crypto Utils** +Path: [`./crypto-utils`](./crypto-utils) + +- Demonstrates how to use **cryptographic operations** in CRE TypeScript workflows using the **Noble crypto libraries**. +- Shows alternatives to Node.js `crypto` module for: hashing (SHA-256, SHA3, Keccak, BLAKE), HMAC, key derivation (PBKDF2, Scrypt, HKDF), symmetric encryption (AES-GCM, ChaCha20-Poly1305), digital signatures (ECDSA, Ed25519), and ECDH key exchange. +- Includes practical examples for Bitcoin and Ethereum address derivation. + +👉 See the block's README for setup, Node.js-to-Noble mapping table, and code examples. --- @@ -63,6 +74,7 @@ Path: [`./read-data-feeds`](./read-data-feeds) * **kv-store**: You want to see an **off-chain write** pattern (AWS S3), secrets usage, SigV4 signing, and a **consensus read → single write** flow. * **read-data-feeds**: You want to **read on-chain data** via contract calls, manage ABIs/bindings, and configure **RPC** access. +* **crypto-utils**: You need to perform **cryptographic operations** (hashing, signing, encryption, key derivation) in a TypeScript workflow. --- diff --git a/building-blocks/crypto-utils/README.md b/building-blocks/crypto-utils/README.md new file mode 100644 index 00000000..39a75585 --- /dev/null +++ b/building-blocks/crypto-utils/README.md @@ -0,0 +1,85 @@ +
+ + Chainlink logo + + +[![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/smartcontractkit/cre-templates/blob/main/LICENSE) +[![CRE Home](https://img.shields.io/static/v1?label=CRE&message=Home&color=blue)](https://chain.link/chainlink-runtime-environment) +[![CRE Documentation](https://img.shields.io/static/v1?label=CRE&message=Docs&color=blue)](https://docs.chain.link/cre) + +
+ +# Crypto Utils - CRE Building Block + +This building block demonstrates how to perform cryptographic operations in CRE workflows using the **Noble crypto libraries** as an alternative to the Node.js `crypto` module. + +## The Problem + +The CRE TypeScript SDK runs on **QuickJS**, a lightweight JavaScript engine that does not support Node.js native modules. This means the standard `crypto` module from Node.js is **not available** in CRE workflows. + +## The Solution + +The [Noble crypto libraries](https://paulmillr.com/noble/) provide pure JavaScript implementations of common cryptographic algorithms. They are: + +- **Pure JavaScript** - No native dependencies, works in QuickJS +- **Audited** - Security-audited implementations +- **Minimal** - Small bundle size (~29KB gzipped for curves) +- **Standards-compliant** - Implements widely-used cryptographic standards + +## Random Bytes + +Noble's `randomBytes()` won't work (needs Web Crypto API), but **CRE provides a `Math.random()` polyfill** using ChaCha8Rng that IS consensus-safe. You can build your own `randomBytes()`: + +```typescript +function randomBytes(length: number): Uint8Array { + const bytes = new Uint8Array(length); + for (let i = 0; i < length; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } + return bytes; +} +``` + +See the [TypeScript README](./crypto-utils-ts/README.md#random-bytes-in-cre) for details. + +--- + +## What's Covered + +This template demonstrates: + +| Category | Noble Package | Operations | +|----------|---------------|------------| +| **Hashing** | `@noble/hashes` | SHA-256, SHA-512, SHA3, Keccak-256, BLAKE2b, BLAKE3, RIPEMD-160 | +| **HMAC** | `@noble/hashes` | HMAC with any hash function | +| **Key Derivation** | `@noble/hashes` | PBKDF2, Scrypt, HKDF | +| **Symmetric Encryption** | `@noble/ciphers` | AES-GCM, ChaCha20-Poly1305 | +| **Digital Signatures** | `@noble/curves` | ECDSA (secp256k1), Ed25519 | +| **Key Exchange** | `@noble/curves` | ECDH, X25519 | +| **Utilities** | `@noble/hashes` | Hex encoding, UTF-8 encoding | +| **Random Bytes** | `Math.random()` polyfill | Consensus-safe PRNG (ChaCha8Rng) | + +## Get Started + +- **TypeScript**: See the [TypeScript README](./crypto-utils-ts/README.md) for detailed setup, usage examples, and a complete Node.js-to-Noble mapping table. + +## Quick Example + +```typescript +// Instead of Node.js crypto: +// const hash = crypto.createHash('sha256').update('hello').digest('hex'); + +// Use Noble: +import { sha256 } from "@noble/hashes/sha2"; +import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils"; + +const hash = bytesToHex(sha256(utf8ToBytes("hello"))); +``` + +## Reference Documentation + +- [Noble Crypto Libraries](https://paulmillr.com/noble/) +- [Noble Hashes GitHub](https://github.com/paulmillr/noble-hashes) +- [Noble Curves GitHub](https://github.com/paulmillr/noble-curves) +- [Noble Ciphers GitHub](https://github.com/paulmillr/noble-ciphers) +- [CRE Documentation](https://docs.chain.link/cre) diff --git a/building-blocks/crypto-utils/crypto-utils-ts/.gitignore b/building-blocks/crypto-utils/crypto-utils-ts/.gitignore new file mode 100644 index 00000000..03bd4129 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/building-blocks/crypto-utils/crypto-utils-ts/README.md b/building-blocks/crypto-utils/crypto-utils-ts/README.md new file mode 100644 index 00000000..cd4e8ec6 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/README.md @@ -0,0 +1,285 @@ +# Crypto Utils - CRE Building Block (TypeScript) + +**⚠️ DISCLAIMER** + +This tutorial represents an educational example to use a Chainlink system, product, or service and is provided to demonstrate how to interact with Chainlink's systems, products, and services to integrate them into your own. This template is provided "AS IS" and "AS AVAILABLE" without warranties of any kind, it has not been audited, and it may be missing key checks or error handling to make the usage of the system, product or service more clear. Do not use the code in this example in a production environment without completing your own audits and application of best practices. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs that are generated due to errors in code. + +--- + +This building block demonstrates how to use cryptographic operations in CRE TypeScript workflows. Since the CRE TypeScript SDK runs on QuickJS (a lightweight JavaScript engine), the standard Node.js `crypto` module is not available. This template shows how to use the **Noble crypto libraries** as a drop-in alternative. + +## Why Noble Libraries? + +The [Noble crypto libraries](https://paulmillr.com/noble/) are: +- **Pure JavaScript** - No native dependencies, works in any JS environment +- **Audited** - Security-audited implementations +- **Minimal** - Small bundle size, tree-shakeable +- **Standards-compliant** - Implements widely-used cryptographic standards + +## Features Demonstrated + +This workflow demonstrates the following cryptographic operations: + +| Category | Operations | +|----------|------------| +| **Hashing** | SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-512, Keccak-256, BLAKE2b, BLAKE3, RIPEMD-160 | +| **HMAC** | HMAC-SHA256, HMAC-SHA512, HMAC-SHA3-256 | +| **Key Derivation** | PBKDF2, Scrypt, HKDF | +| **Symmetric Encryption** | AES-256-GCM, ChaCha20-Poly1305 | +| **Digital Signatures** | ECDSA (secp256k1), Ed25519 | +| **Key Exchange** | ECDH (secp256k1), X25519 | +| **Utilities** | Hex encoding/decoding, UTF-8 encoding | +| **Random Bytes** | Using CRE's ChaCha8Rng-based `Math.random()` polyfill | +| **Real-world Examples** | Bitcoin address derivation, Ethereum address derivation | + +--- + +## Random Bytes in CRE + +**Noble's `randomBytes()` won't work** because it requires `crypto.getRandomValues` (Web Crypto API) which QuickJS doesn't support. + +**However, CRE provides a `Math.random()` polyfill** that IS consensus-safe: + +- **Implementation**: ChaCha8Rng (cryptographic PRNG) in the Javy WASM plugin +- **Seeding**: Host runtime provides seed via `random_seed()` import +- **Consensus-safe**: Same seed produces same sequence across all DON nodes +- **Mode isolation**: Different execution modes get independent RNG streams + +### Using Random Bytes in CRE + +Create your own `randomBytes()` function using CRE's `Math.random()`: + +```typescript +function randomBytes(length: number): Uint8Array { + const bytes = new Uint8Array(length); + for (let i = 0; i < length; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } + return bytes; +} + +// Use it for keys and nonces +const aesKey = randomBytes(32); // 256-bit key +const nonce = randomBytes(12); // 96-bit nonce +const privateKey = randomBytes(32); // EC private key +``` + +--- + +## Node.js crypto to Noble Library Mapping + +Use this table to migrate from Node.js `crypto` module to Noble libraries: + +| Node.js crypto | Noble Library | Import | Code Reference | +|----------------|---------------|--------|----------------| +| `crypto.createHash('sha256')` | `@noble/hashes/sha2` | `import { sha256 } from "@noble/hashes/sha2"` | [main.ts:89](./workflow/main.ts#L89) | +| `crypto.createHash('sha512')` | `@noble/hashes/sha2` | `import { sha512 } from "@noble/hashes/sha2"` | [main.ts:95](./workflow/main.ts#L95) | +| `crypto.createHash('sha3-256')` | `@noble/hashes/sha3` | `import { sha3_256 } from "@noble/hashes/sha3"` | [main.ts:99](./workflow/main.ts#L99) | +| `crypto.createHash('ripemd160')` | `@noble/hashes/ripemd160` | `import { ripemd160 } from "@noble/hashes/ripemd160"` | [main.ts:117](./workflow/main.ts#L117) | +| `crypto.createHmac('sha256', key)` | `@noble/hashes/hmac` | `import { hmac } from "@noble/hashes/hmac"` | [main.ts:135](./workflow/main.ts#L135) | +| `crypto.pbkdf2()` | `@noble/hashes/pbkdf2` | `import { pbkdf2 } from "@noble/hashes/pbkdf2"` | [main.ts:155](./workflow/main.ts#L155) | +| `crypto.scrypt()` | `@noble/hashes/scrypt` | `import { scrypt } from "@noble/hashes/scrypt"` | [main.ts:160](./workflow/main.ts#L160) | +| `crypto.hkdf()` | `@noble/hashes/hkdf` | `import { hkdf } from "@noble/hashes/hkdf"` | [main.ts:167](./workflow/main.ts#L167) | +| `crypto.createCipheriv('aes-256-gcm')` | `@noble/ciphers/aes` | `import { gcm } from "@noble/ciphers/aes"` | [main.ts:180](./workflow/main.ts#L180) | +| `crypto.createCipheriv('chacha20-poly1305')` | `@noble/ciphers/chacha` | `import { chacha20poly1305 } from "@noble/ciphers/chacha"` | [main.ts:192](./workflow/main.ts#L192) | +| `crypto.sign()` with ECDSA | `@noble/curves/secp256k1` | `secp256k1.sign(msgHash, privateKey)` | [main.ts:227](./workflow/main.ts#L227) | +| `crypto.verify()` with ECDSA | `@noble/curves/secp256k1` | `secp256k1.verify(signature, msgHash, publicKey)` | [main.ts:238](./workflow/main.ts#L238) | +| `crypto.sign()` with Ed25519 | `@noble/curves/ed25519` | `ed25519.sign(message, privateKey)` | [main.ts:261](./workflow/main.ts#L261) | +| `crypto.diffieHellman()` | `@noble/curves` | `secp256k1.getSharedSecret()` or `x25519.getSharedSecret()` | [main.ts:282](./workflow/main.ts#L282) | +| `crypto.randomBytes()` | Custom using `Math.random()` | See [Random Bytes in CRE](#random-bytes-in-cre) | [main.ts:68](./workflow/main.ts#L68) | +| `Buffer.from(hex, 'hex')` | `@noble/hashes/utils` | `import { hexToBytes } from "@noble/hashes/utils"` | [main.ts:320](./workflow/main.ts#L320) | +| `buffer.toString('hex')` | `@noble/hashes/utils` | `import { bytesToHex } from "@noble/hashes/utils"` | [main.ts:321](./workflow/main.ts#L321) | + +--- + +## Dependencies + +The following Noble packages are used: + +```json +{ + "@noble/hashes": "^1.7.1", + "@noble/curves": "^1.8.1", + "@noble/ciphers": "^1.2.1" +} +``` + +--- + +## Setup and Prerequisites + +1. **Install CRE CLI** + ```bash + # See https://docs.chain.link/cre for installation instructions + ``` + +2. **Login to CRE** + ```bash + cre login + ``` + +3. **Install Bun** (if not already installed) + ```bash + # See https://bun.sh/docs/installation + ``` + +4. **Install dependencies** + ```bash + cd building-blocks/crypto-utils/crypto-utils-ts/workflow + bun install + ``` + +--- + +## Running the Workflow + +### Simulate the workflow + +From the project root directory (`crypto-utils-ts`): + +```bash +cre workflow simulate workflow +``` + +--- + +## Example Output + +When the workflow runs, it logs the output of each cryptographic operation: + +``` +======================================== +NOBLE CRYPTO LIBRARIES DEMO +Alternative to Node.js crypto module +Compatible with QuickJS / CRE Workflows +======================================== + +=== HASHING FUNCTIONS === +SHA-256: 828ce562b0a732143cddc2895bdb9b4ea98293805b24491853c2970186a36f88 +SHA-384: 0a5d403a2d3ca36332a533ea6dfb85fd5facaa3120f8a41cb63cf8ea4da2f3fa... +SHA-512: 81d3babc839e8894eb89e7579050d14ee36e6a8599696e551a89e576ab31e2e5... +SHA3-256: d2a7297f24e4da526fb97568b69f8c1bbe53574d1383d2533700e16c551dce45 +Keccak-256 (Ethereum): ec4fa95ae783668d4468af54f9ec9630729ac6a9b4550acb26278b6507f7cf51 +BLAKE2b-256: 8a3030f77cae72dc512c41ab1645a179f6e4d31533ffe8a417c37c1b004eaddb +BLAKE3: 3cc4857ee80f9820a775feed897ab77d6bbc6202c44a885f1281c616df6b67c2 +RIPEMD-160: 60a86f1115366d6f78a212b5239e8c8f135d6bed + +=== HMAC (Hash-based Message Authentication Code) === +HMAC-SHA256: e3c9d42ece6cbc90bef8b2ef280bd073505941800b250a1d1121125d608311a9 +HMAC-SHA512: 6cf6da999feb5cfdcfd5517b7e2ad5bc13caeb492ba7613a8fbaf0744947d731... + +=== KEY DERIVATION FUNCTIONS === +PBKDF2-SHA256 (1k iterations): d484e54ed63ba6b9ad7f1750e566081efae6a9ef93986a03982956da1570b98e +Scrypt (N=1024, r=8, p=1): b7ba286855e8a2f502b47aed41702123b490e4688c5056e90802b2ab7988dba4 +HKDF-SHA256: 21b286fe4324bdbe19319aa7ac3d894757a1c66b3752c4a294b5e91165260ac4 + +=== SYMMETRIC ENCRYPTION (AEAD) === +NOTE: Using deterministic keys for demo. In production, use secure random keys. +AES-256-GCM Ciphertext: 237133d47a1324a7f4e7144e56e8a985f8a8d0dfa42403d06d9383e9b9b61353... +AES-256-GCM Decrypted: Sensitive data to encrypt +ChaCha20-Poly1305 Ciphertext: 0e95af78139c7050d5daa94f2d252ab7aed590e5f0174de932c36e3a6e19cf20... +ChaCha20-Poly1305 Decrypted: Sensitive data to encrypt + +=== ECDSA with secp256k1 (Bitcoin/Ethereum) === +Private Key: 67211ff271555e843683256b426a4453e4104e09dff3064d59a9486044987a89 +Public Key (compressed): 03544cfb0431489a1d4a551b1cd9eca0445b68a15d9a1a02c36aceec17850a4559 +Signature Valid: true +Recovered Public Key: 03544cfb0431489a1d4a551b1cd9eca0445b68a15d9a1a02c36aceec17850a4559 + +=== Ed25519 Signatures (Modern, Fast) === +Private Key: c6dc9f6ae2eb26ba9c9ea78407fb16d36cab14af6b22e94f08ddc706cef782cb +Public Key: 4895153ca98d10fda4e516ea74575e33a0ba54634cea73c8c1c2b39f6b124521 +Signature Valid: true + +=== ECDH Key Exchange === +Alice's Shared Secret: 02529bb8e1a3106f7c7097211aee7598d37f1f5735218e8fff926810c984f55c10 +Bob's Shared Secret: 02529bb8e1a3106f7c7097211aee7598d37f1f5735218e8fff926810c984f55c10 +Secrets Match: true + +=== UTILITY FUNCTIONS === +NOTE: randomBytes() not available in QuickJS (no crypto.getRandomValues) +Original Hex: 48656c6c6f2c20576f726c6421 +Decoded Text: Hello, World! + +=== BITCOIN ADDRESS DERIVATION EXAMPLE === +Hash160 (RIPEMD160(SHA256(pubkey))): 01ec9bb0efbb084ff780954eed1ca941d9567b84 + +=== ETHEREUM ADDRESS DERIVATION EXAMPLE === +Ethereum Address: 0xa181b2fcc624d496c7e250c96449f55e716efe12 + +======================================== +DEMO COMPLETE +======================================== +``` + +--- + +## Use Cases + +### 1. Message Signing and Verification +Sign messages or transactions for blockchain interactions: +```typescript +import { secp256k1 } from "@noble/curves/secp256k1"; +import { sha256 } from "@noble/hashes/sha2"; +import { utf8ToBytes } from "@noble/hashes/utils"; + +const messageHash = sha256(utf8ToBytes("Transaction data")); +const signature = secp256k1.sign(messageHash, privateKey); +const isValid = secp256k1.verify(signature, messageHash, publicKey); +``` + +### 2. Password Hashing +Securely hash passwords for storage: +```typescript +import { scrypt } from "@noble/hashes/scrypt"; + +// Use higher N (2^14 or more) in production for better security +const hashedPassword = scrypt(password, salt, { N: 2 ** 14, r: 8, p: 1, dkLen: 32 }); +``` + +### 3. Data Encryption +Encrypt sensitive data before storage or transmission: +```typescript +import { gcm } from "@noble/ciphers/aes"; + +// Key and nonce should come from secure sources (see Random Bytes Limitation) +const cipher = gcm(key, nonce); +const encrypted = cipher.encrypt(plaintext); +const decrypted = cipher.decrypt(encrypted); +``` + +### 4. Ethereum/Bitcoin Address Generation +Derive addresses from private keys: +```typescript +import { secp256k1 } from "@noble/curves/secp256k1"; +import { keccak_256 } from "@noble/hashes/sha3"; +import { bytesToHex } from "@noble/hashes/utils"; + +const publicKey = secp256k1.getPublicKey(privateKey, false).slice(1); +const address = "0x" + bytesToHex(keccak_256(publicKey).slice(-20)); +``` + +### 5. Secure Key Exchange (ECDH) +Establish shared secrets between parties: +```typescript +import { x25519 } from "@noble/curves/ed25519"; + +const sharedSecret = x25519.getSharedSecret(myPrivateKey, theirPublicKey); +// Use HKDF to derive encryption keys from the shared secret +``` + +--- + +## Reference Documentation + +- [CRE Documentation](https://docs.chain.link/cre) +- [Noble Hashes](https://github.com/paulmillr/noble-hashes) +- [Noble Curves](https://github.com/paulmillr/noble-curves) +- [Noble Ciphers](https://github.com/paulmillr/noble-ciphers) +- [Noble Overview](https://paulmillr.com/noble/) + +--- + +## License + +MIT - see the repository's [LICENSE](https://github.com/smartcontractkit/cre-templates/blob/main/LICENSE). diff --git a/building-blocks/crypto-utils/crypto-utils-ts/project.yaml b/building-blocks/crypto-utils/crypto-utils-ts/project.yaml new file mode 100644 index 00000000..90938415 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/project.yaml @@ -0,0 +1,36 @@ +# ========================================================================== +# CRE PROJECT SETTINGS FILE +# ========================================================================== +# Project-specific settings for CRE CLI targets. +# Each target defines cre-cli, account, and rpcs groups. +# +# Example custom target: +# my-target: +# account: +# workflow-owner-address: "0x123..." # Optional: Owner wallet/MSIG address (used for --unsigned transactions) +# rpcs: +# - chain-name: ethereum-testnet-sepolia # Required if your workflow interacts with this chain +# url: "" +# +# Experimental chains (automatically used by the simulator when present): +# Use this for chains not yet in official chain-selectors (e.g., hackathons, new chain integrations). +# In your workflow, reference the chain as evm:ChainSelector:@1.0.0 +# +# experimental-chains: +# - chain-id: 12345 # The numeric chain ID +# rpc-url: "https://rpc.example.com" # RPC endpoint URL +# forwarder: "0x..." # Forwarder contract address on the chain + +# ========================================================================== +staging-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + +# ========================================================================== +production-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com diff --git a/building-blocks/crypto-utils/crypto-utils-ts/secrets.yaml b/building-blocks/crypto-utils/crypto-utils-ts/secrets.yaml new file mode 100644 index 00000000..63307f2f --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/secrets.yaml @@ -0,0 +1,3 @@ +secretsNames: + SECRET_ADDRESS: + - SECRET_ADDRESS_ALL diff --git a/building-blocks/crypto-utils/crypto-utils-ts/workflow/config.production.json b/building-blocks/crypto-utils/crypto-utils-ts/workflow/config.production.json new file mode 100644 index 00000000..1a360cb3 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/workflow/config.production.json @@ -0,0 +1,3 @@ +{ + "schedule": "*/30 * * * * *" +} diff --git a/building-blocks/crypto-utils/crypto-utils-ts/workflow/config.staging.json b/building-blocks/crypto-utils/crypto-utils-ts/workflow/config.staging.json new file mode 100644 index 00000000..1a360cb3 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/workflow/config.staging.json @@ -0,0 +1,3 @@ +{ + "schedule": "*/30 * * * * *" +} diff --git a/building-blocks/crypto-utils/crypto-utils-ts/workflow/main.ts b/building-blocks/crypto-utils/crypto-utils-ts/workflow/main.ts new file mode 100644 index 00000000..23272883 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/workflow/main.ts @@ -0,0 +1,460 @@ +import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"; + +// ============================================================================ +// Noble Hashes - Cryptographic Hash Functions +// Alternative to: crypto.createHash() +// ============================================================================ +import { sha256, sha384, sha512 } from "@noble/hashes/sha2"; +import { sha3_256, sha3_512, keccak_256 } from "@noble/hashes/sha3"; +import { blake2b } from "@noble/hashes/blake2b"; +import { blake3 } from "@noble/hashes/blake3"; +import { ripemd160 } from "@noble/hashes/ripemd160"; + +// ============================================================================ +// Noble Hashes - HMAC +// Alternative to: crypto.createHmac() +// ============================================================================ +import { hmac } from "@noble/hashes/hmac"; + +// ============================================================================ +// Noble Hashes - Key Derivation Functions +// Alternative to: crypto.pbkdf2(), crypto.scrypt(), crypto.hkdf() +// ============================================================================ +import { pbkdf2 } from "@noble/hashes/pbkdf2"; +import { scrypt } from "@noble/hashes/scrypt"; +import { hkdf } from "@noble/hashes/hkdf"; + +// ============================================================================ +// Noble Hashes - Utilities +// Alternative to: Buffer.from(hex, 'hex'), buffer.toString('hex') +// NOTE: randomBytes() is NOT available in QuickJS (no crypto.getRandomValues) +// ============================================================================ +import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils"; + +// ============================================================================ +// Noble Curves - Elliptic Curve Cryptography +// Alternative to: crypto.generateKeyPair(), crypto.sign(), crypto.verify() +// ============================================================================ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { ed25519 } from "@noble/curves/ed25519"; +import { x25519 } from "@noble/curves/ed25519"; + +// ============================================================================ +// Noble Ciphers - Symmetric Encryption +// Alternative to: crypto.createCipheriv(), crypto.createDecipheriv() +// ============================================================================ +import { gcm } from "@noble/ciphers/aes"; +import { chacha20poly1305 } from "@noble/ciphers/chacha"; + +type Config = { + schedule: string; +}; + +// ============================================================================ +// Random Bytes Generation for CRE +// +// Noble's randomBytes() uses crypto.getRandomValues (Web Crypto API) which is +// NOT available in QuickJS. However, CRE provides a Math.random() polyfill: +// +// - Implementation: ChaCha8Rng (cryptographic PRNG) in the Javy WASM plugin +// - Seeding: Host runtime provides seed via random_seed() import +// - Consensus-safe: Same seed produces same sequence across all DON nodes +// - Mode isolation: Different execution modes get independent RNG streams +// +// Below we provide randomBytes() using CRE's Math.random() polyfill. +// ============================================================================ + +/** + * Generate random bytes using CRE's consensus-safe Math.random() polyfill. + * + * CRE replaces Math.random() with a deterministic ChaCha8Rng PRNG seeded by + * the host runtime. This ensures all DON nodes generate the same sequence + * of random values for consensus. + */ +function randomBytes(length: number): Uint8Array { + const bytes = new Uint8Array(length); + for (let i = 0; i < length; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } + return bytes; +} + +/** + * Derive deterministic bytes from a seed (for reproducible demo output). + * Used in address derivation examples so output matches documentation. + */ +function deriveBytes(seed: string, length: number): Uint8Array { + const hash = sha512(utf8ToBytes(seed)); + if (length <= 64) { + return hash.slice(0, length); + } + const result = new Uint8Array(length); + let offset = 0; + let counter = 0; + while (offset < length) { + const block = sha512(utf8ToBytes(`${seed}-${counter}`)); + const toCopy = Math.min(64, length - offset); + result.set(block.slice(0, toCopy), offset); + offset += toCopy; + counter++; + } + return result; +} + +// ============================================================================ +// Demo Functions for Each Crypto Category +// ============================================================================ + +function demoHashing(runtime: Runtime): void { + runtime.log("=== HASHING FUNCTIONS ==="); + + const message = utf8ToBytes("Hello, CRE Workflow!"); + + // SHA-2 Family (most common) + const sha256Hash = sha256(message); + runtime.log(`SHA-256: ${bytesToHex(sha256Hash)}`); + + const sha384Hash = sha384(message); + runtime.log(`SHA-384: ${bytesToHex(sha384Hash)}`); + + const sha512Hash = sha512(message); + runtime.log(`SHA-512: ${bytesToHex(sha512Hash)}`); + + // SHA-3 Family + const sha3_256Hash = sha3_256(message); + runtime.log(`SHA3-256: ${bytesToHex(sha3_256Hash)}`); + + const sha3_512Hash = sha3_512(message); + runtime.log(`SHA3-512: ${bytesToHex(sha3_512Hash)}`); + + // Keccak-256 (used in Ethereum for addresses and tx hashes) + const keccak256Hash = keccak_256(message); + runtime.log(`Keccak-256 (Ethereum): ${bytesToHex(keccak256Hash)}`); + + // BLAKE2b (fast, secure, used in many blockchains) + const blake2bHash = blake2b(message, { dkLen: 32 }); + runtime.log(`BLAKE2b-256: ${bytesToHex(blake2bHash)}`); + + // BLAKE3 (fastest secure hash) + const blake3Hash = blake3(message); + runtime.log(`BLAKE3: ${bytesToHex(blake3Hash)}`); + + // RIPEMD-160 (used in Bitcoin addresses) + const ripemd160Hash = ripemd160(message); + runtime.log(`RIPEMD-160: ${bytesToHex(ripemd160Hash)}`); + + // Incremental hashing (for large data) + const incrementalHash = sha256.create() + .update(utf8ToBytes("Hello, ")) + .update(utf8ToBytes("CRE Workflow!")) + .digest(); + runtime.log(`SHA-256 (incremental): ${bytesToHex(incrementalHash)}`); +} + +function demoHmac(runtime: Runtime): void { + runtime.log("=== HMAC (Hash-based Message Authentication Code) ==="); + + const key = utf8ToBytes("my-secret-key-for-hmac"); + const message = utf8ToBytes("Message to authenticate"); + + // HMAC-SHA256 (most common) + const hmacSha256 = hmac(sha256, key, message); + runtime.log(`HMAC-SHA256: ${bytesToHex(hmacSha256)}`); + + // HMAC-SHA512 + const hmacSha512 = hmac(sha512, key, message); + runtime.log(`HMAC-SHA512: ${bytesToHex(hmacSha512)}`); + + // HMAC-SHA3-256 + const hmacSha3 = hmac(sha3_256, key, message); + runtime.log(`HMAC-SHA3-256: ${bytesToHex(hmacSha3)}`); +} + +function demoKeyDerivation(runtime: Runtime): void { + runtime.log("=== KEY DERIVATION FUNCTIONS ==="); + + const password = utf8ToBytes("user-password"); + const salt = utf8ToBytes("random-salt-value"); + + // PBKDF2 - Password-Based Key Derivation Function 2 + // Good for: Password hashing, deriving encryption keys from passwords + // NOTE: Using lower iterations (1000) for demo speed. Use 100000+ in production. + const pbkdf2Key = pbkdf2(sha256, password, salt, { c: 1000, dkLen: 32 }); + runtime.log(`PBKDF2-SHA256 (1k iterations): ${bytesToHex(pbkdf2Key)}`); + + // Scrypt - Memory-hard password hashing + // Good for: Password storage (resistant to GPU/ASIC attacks) + // NOTE: Using lower N (2^10) for demo speed. Use 2^14 or higher in production. + const scryptKey = scrypt(password, salt, { N: 2 ** 10, r: 8, p: 1, dkLen: 32 }); + runtime.log(`Scrypt (N=1024, r=8, p=1): ${bytesToHex(scryptKey)}`); + + // HKDF - HMAC-based Key Derivation Function + // Good for: Deriving multiple keys from a single secret + const inputKeyMaterial = utf8ToBytes("shared-secret-from-ecdh"); + const info = utf8ToBytes("encryption-key"); + const hkdfKey = hkdf(sha256, inputKeyMaterial, salt, info, 32); + runtime.log(`HKDF-SHA256: ${bytesToHex(hkdfKey)}`); +} + +function demoSymmetricEncryption(runtime: Runtime): void { + runtime.log("=== SYMMETRIC ENCRYPTION (AEAD) ==="); + runtime.log("Using randomBytes() with CRE's ChaCha8Rng-based Math.random() polyfill."); + + const plaintext = utf8ToBytes("Sensitive data to encrypt"); + + // AES-256-GCM - Industry standard authenticated encryption + const aesKey = randomBytes(32); // 256-bit key + const aesNonce = randomBytes(12); // 96-bit nonce for GCM + + const aesGcm = gcm(aesKey, aesNonce); + const aesCiphertext = aesGcm.encrypt(plaintext); + const aesDecrypted = aesGcm.decrypt(aesCiphertext); + + runtime.log(`AES-256-GCM Key: ${bytesToHex(aesKey)}`); + runtime.log(`AES-256-GCM Nonce: ${bytesToHex(aesNonce)}`); + runtime.log(`AES-256-GCM Ciphertext: ${bytesToHex(aesCiphertext)}`); + runtime.log(`AES-256-GCM Decrypted: ${new TextDecoder().decode(aesDecrypted)}`); + + // ChaCha20-Poly1305 - Modern alternative to AES-GCM + // Faster in software, no timing attacks, used by TLS 1.3 + const chachaKey = randomBytes(32); // 256-bit key + const chachaNonce = randomBytes(12); // 96-bit nonce + + const chacha = chacha20poly1305(chachaKey, chachaNonce); + const chachaCiphertext = chacha.encrypt(plaintext); + const chachaDecrypted = chacha.decrypt(chachaCiphertext); + + runtime.log(`ChaCha20-Poly1305 Key: ${bytesToHex(chachaKey)}`); + runtime.log(`ChaCha20-Poly1305 Nonce: ${bytesToHex(chachaNonce)}`); + runtime.log(`ChaCha20-Poly1305 Ciphertext: ${bytesToHex(chachaCiphertext)}`); + runtime.log(`ChaCha20-Poly1305 Decrypted: ${new TextDecoder().decode(chachaDecrypted)}`); +} + +function demoEcdsaSecp256k1(runtime: Runtime): void { + runtime.log("=== ECDSA with secp256k1 (Bitcoin/Ethereum) ==="); + runtime.log("Using randomBytes() with CRE's ChaCha8Rng-based Math.random() polyfill."); + + // Generate random private key using CRE's consensus-safe PRNG + const privateKey = randomBytes(32); + runtime.log(`Private Key: ${bytesToHex(privateKey)}`); + + // Derive public key (compressed format - 33 bytes) + const publicKey = secp256k1.getPublicKey(privateKey); + runtime.log(`Public Key (compressed): ${bytesToHex(publicKey)}`); + + // Derive public key (uncompressed format - 65 bytes) + const publicKeyUncompressed = secp256k1.getPublicKey(privateKey, false); + runtime.log(`Public Key (uncompressed): ${bytesToHex(publicKeyUncompressed)}`); + + // Sign a message (message should be a 32-byte hash) + const messageHash = sha256(utf8ToBytes("Transaction data to sign")); + const signature = secp256k1.sign(messageHash, privateKey); + + runtime.log(`Signature (r): ${signature.r.toString(16)}`); + runtime.log(`Signature (s): ${signature.s.toString(16)}`); + runtime.log(`Signature (recovery): ${signature.recovery}`); + runtime.log(`Signature (compact): ${bytesToHex(signature.toCompactRawBytes())}`); + runtime.log(`Signature (DER): ${bytesToHex(signature.toDERRawBytes())}`); + + // Verify the signature + const isValid = secp256k1.verify(signature, messageHash, publicKey); + runtime.log(`Signature Valid: ${isValid}`); + + // Recover public key from signature (useful for Ethereum) + const recoveredPublicKey = signature.recoverPublicKey(messageHash); + runtime.log(`Recovered Public Key: ${bytesToHex(recoveredPublicKey.toRawBytes())}`); +} + +function demoEd25519(runtime: Runtime): void { + runtime.log("=== Ed25519 Signatures (Modern, Fast) ==="); + runtime.log("Using randomBytes() with CRE's ChaCha8Rng-based Math.random() polyfill."); + + // Generate random private key using CRE's consensus-safe PRNG + const privateKey = randomBytes(32); + runtime.log(`Private Key: ${bytesToHex(privateKey)}`); + + // Derive public key + const publicKey = ed25519.getPublicKey(privateKey); + runtime.log(`Public Key: ${bytesToHex(publicKey)}`); + + // Sign a message (Ed25519 hashes internally, no pre-hashing needed) + const message = utf8ToBytes("Message to sign with Ed25519"); + const signature = ed25519.sign(message, privateKey); + runtime.log(`Signature: ${bytesToHex(signature)}`); + + // Verify the signature + const isValid = ed25519.verify(signature, message, publicKey); + runtime.log(`Signature Valid: ${isValid}`); +} + +function demoEcdh(runtime: Runtime): void { + runtime.log("=== ECDH Key Exchange ==="); + runtime.log("Using randomBytes() with CRE's ChaCha8Rng-based Math.random() polyfill."); + + // ECDH with secp256k1 (Bitcoin/Ethereum compatible) + runtime.log("--- secp256k1 ECDH ---"); + const alicePrivKey = randomBytes(32); + const alicePubKey = secp256k1.getPublicKey(alicePrivKey); + + const bobPrivKey = randomBytes(32); + const bobPubKey = secp256k1.getPublicKey(bobPrivKey); + + runtime.log(`Alice Public Key: ${bytesToHex(alicePubKey)}`); + runtime.log(`Bob Public Key: ${bytesToHex(bobPubKey)}`); + + // Both parties derive the same shared secret + const aliceSharedSecret = secp256k1.getSharedSecret(alicePrivKey, bobPubKey); + const bobSharedSecret = secp256k1.getSharedSecret(bobPrivKey, alicePubKey); + + runtime.log(`Alice's Shared Secret: ${bytesToHex(aliceSharedSecret)}`); + runtime.log(`Bob's Shared Secret: ${bytesToHex(bobSharedSecret)}`); + runtime.log(`Secrets Match: ${bytesToHex(aliceSharedSecret) === bytesToHex(bobSharedSecret)}`); + + // X25519 ECDH (Modern, recommended for new applications) + runtime.log("--- X25519 ECDH (Curve25519) ---"); + const aliceX25519Priv = randomBytes(32); + const aliceX25519Pub = x25519.getPublicKey(aliceX25519Priv); + + const bobX25519Priv = randomBytes(32); + const bobX25519Pub = x25519.getPublicKey(bobX25519Priv); + + runtime.log(`Alice X25519 Public Key: ${bytesToHex(aliceX25519Pub)}`); + runtime.log(`Bob X25519 Public Key: ${bytesToHex(bobX25519Pub)}`); + + const aliceX25519Shared = x25519.getSharedSecret(aliceX25519Priv, bobX25519Pub); + const bobX25519Shared = x25519.getSharedSecret(bobX25519Priv, aliceX25519Pub); + + runtime.log(`Alice's X25519 Shared Secret: ${bytesToHex(aliceX25519Shared)}`); + runtime.log(`Bob's X25519 Shared Secret: ${bytesToHex(bobX25519Shared)}`); + runtime.log(`X25519 Secrets Match: ${bytesToHex(aliceX25519Shared) === bytesToHex(bobX25519Shared)}`); +} + +function demoUtilities(runtime: Runtime): void { + runtime.log("=== UTILITY FUNCTIONS ==="); + + // Random bytes using CRE's Math.random() polyfill (ChaCha8Rng PRNG) + // Note: Noble's randomBytes() won't work, but our custom one does! + runtime.log("Random bytes using CRE's ChaCha8Rng-based Math.random():"); + const randomData = randomBytes(16); + runtime.log(`Random Bytes (16): ${bytesToHex(randomData)}`); + + // Hex encoding/decoding + const originalHex = "48656c6c6f2c20576f726c6421"; // "Hello, World!" in hex + const decodedBytes = hexToBytes(originalHex); + const reEncodedHex = bytesToHex(decodedBytes); + runtime.log(`Original Hex: ${originalHex}`); + runtime.log(`Decoded Text: ${new TextDecoder().decode(decodedBytes)}`); + runtime.log(`Re-encoded Hex: ${reEncodedHex}`); + + // UTF-8 encoding + const text = "Hello, World!"; + const utf8Bytes = utf8ToBytes(text); + runtime.log(`UTF-8 Text: ${text}`); + runtime.log(`UTF-8 Bytes (hex): ${bytesToHex(utf8Bytes)}`); + runtime.log(`UTF-8 Byte Length: ${utf8Bytes.length}`); +} + +function demoBitcoinAddressDerivation(runtime: Runtime): void { + runtime.log("=== BITCOIN ADDRESS DERIVATION EXAMPLE ==="); + + // Use a deterministic private key for demo + const privateKey = deriveBytes("demo-bitcoin-private-key", 32); + runtime.log(`Private Key (WIF would be derived from this): ${bytesToHex(privateKey)}`); + + // Get compressed public key + const publicKey = secp256k1.getPublicKey(privateKey, true); + runtime.log(`Compressed Public Key: ${bytesToHex(publicKey)}`); + + // Bitcoin P2PKH address derivation: RIPEMD160(SHA256(publicKey)) + const sha256Hash = sha256(publicKey); + const hash160 = ripemd160(sha256Hash); + runtime.log(`Hash160 (RIPEMD160(SHA256(pubkey))): ${bytesToHex(hash160)}`); + + // Note: To get actual Bitcoin address, you'd need Base58Check encoding + // which requires additional libraries (like @scure/base) +} + +function demoEthereumAddressDerivation(runtime: Runtime): void { + runtime.log("=== ETHEREUM ADDRESS DERIVATION EXAMPLE ==="); + + // Use a deterministic private key for demo + const privateKey = deriveBytes("demo-ethereum-private-key", 32); + runtime.log(`Private Key: ${bytesToHex(privateKey)}`); + + // Get uncompressed public key (remove 0x04 prefix) + const publicKeyUncompressed = secp256k1.getPublicKey(privateKey, false); + const publicKeyWithoutPrefix = publicKeyUncompressed.slice(1); // Remove 0x04 prefix + runtime.log(`Public Key (without prefix): ${bytesToHex(publicKeyWithoutPrefix)}`); + + // Ethereum address: last 20 bytes of Keccak256(publicKey) + const keccakHash = keccak_256(publicKeyWithoutPrefix); + const ethereumAddress = keccakHash.slice(-20); // Last 20 bytes + runtime.log(`Ethereum Address: 0x${bytesToHex(ethereumAddress)}`); +} + +// ============================================================================ +// Main Workflow Handler +// ============================================================================ + +const onCronTrigger = (runtime: Runtime): string => { + runtime.log("========================================"); + runtime.log("NOBLE CRYPTO LIBRARIES DEMO"); + runtime.log("Alternative to Node.js crypto module"); + runtime.log("Compatible with QuickJS / CRE Workflows"); + runtime.log("========================================"); + runtime.log(""); + + demoHashing(runtime); + runtime.log(""); + + demoHmac(runtime); + runtime.log(""); + + demoKeyDerivation(runtime); + runtime.log(""); + + demoSymmetricEncryption(runtime); + runtime.log(""); + + demoEcdsaSecp256k1(runtime); + runtime.log(""); + + demoEd25519(runtime); + runtime.log(""); + + demoEcdh(runtime); + runtime.log(""); + + demoUtilities(runtime); + runtime.log(""); + + demoBitcoinAddressDerivation(runtime); + runtime.log(""); + + demoEthereumAddressDerivation(runtime); + runtime.log(""); + + runtime.log("========================================"); + runtime.log("DEMO COMPLETE"); + runtime.log("========================================"); + + return "Crypto demo completed successfully"; +}; + +const initWorkflow = (config: Config) => { + const cron = new CronCapability(); + + return [ + handler( + cron.trigger( + { schedule: config.schedule } + ), + onCronTrigger + ), + ]; +}; + +export async function main() { + const runner = await Runner.newRunner(); + await runner.run(initWorkflow); +} diff --git a/building-blocks/crypto-utils/crypto-utils-ts/workflow/package.json b/building-blocks/crypto-utils/crypto-utils-ts/workflow/package.json new file mode 100644 index 00000000..376d0f73 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/workflow/package.json @@ -0,0 +1,19 @@ +{ + "name": "typescript-simple-template", + "version": "1.0.0", + "main": "dist/main.js", + "private": true, + "scripts": { + "postinstall": "bun x cre-setup" + }, + "license": "UNLICENSED", + "dependencies": { + "@chainlink/cre-sdk": "^1.0.7", + "@noble/hashes": "^1.7.1", + "@noble/curves": "^1.8.1", + "@noble/ciphers": "^1.2.1" + }, + "devDependencies": { + "@types/bun": "1.2.21" + } +} diff --git a/building-blocks/crypto-utils/crypto-utils-ts/workflow/tsconfig.json b/building-blocks/crypto-utils/crypto-utils-ts/workflow/tsconfig.json new file mode 100644 index 00000000..840fdc79 --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/workflow/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "main.ts" + ] +} diff --git a/building-blocks/crypto-utils/crypto-utils-ts/workflow/workflow.yaml b/building-blocks/crypto-utils/crypto-utils-ts/workflow/workflow.yaml new file mode 100644 index 00000000..45b0e62b --- /dev/null +++ b/building-blocks/crypto-utils/crypto-utils-ts/workflow/workflow.yaml @@ -0,0 +1,34 @@ +# ========================================================================== +# CRE WORKFLOW SETTINGS FILE +# ========================================================================== +# Workflow-specific settings for CRE CLI targets. +# Each target defines user-workflow and workflow-artifacts groups. +# Settings here override CRE Project Settings File values. +# +# Example custom target: +# my-target: +# user-workflow: +# workflow-name: "MyExampleWorkflow" # Required: Workflow Registry name +# workflow-artifacts: +# workflow-path: "./main.ts" # Path to workflow entry point +# config-path: "./config.yaml" # Path to config file +# secrets-path: "../secrets.yaml" # Path to secrets file (project root by default) + +# ========================================================================== +staging-settings: + user-workflow: + workflow-name: "workflow-staging" + workflow-artifacts: + workflow-path: "./main.ts" + config-path: "./config.staging.json" + secrets-path: "" + + +# ========================================================================== +production-settings: + user-workflow: + workflow-name: "workflow-production" + workflow-artifacts: + workflow-path: "./main.ts" + config-path: "./config.production.json" + secrets-path: "" \ No newline at end of file