426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
239 lines
8.8 KiB
Swift
239 lines
8.8 KiB
Swift
import Foundation
|
|
import JavaScriptCore
|
|
import CryptoKit
|
|
import CommonCrypto
|
|
|
|
/// Implements the `crypto_*` host functions from `BRIDGE_CONTRACT.md`.
|
|
///
|
|
/// SHA/HMAC/AES-GCM go through CryptoKit. PBKDF2 falls through to
|
|
/// CommonCrypto's `CCKeyDerivationPBKDF` because CryptoKit doesn't expose
|
|
/// PBKDF2 directly.
|
|
public final class CryptoBridge {
|
|
public init() {}
|
|
|
|
public func install(into ctx: JSContext) {
|
|
ctx.installBridgeFunction(name: "crypto_random_bytes") { args in
|
|
guard let len = args.first?.toNumber()?.intValue, len > 0 else {
|
|
return ctx.newUint8Array(Data())
|
|
}
|
|
var bytes = [UInt8](repeating: 0, count: len)
|
|
let status = SecRandomCopyBytes(kSecRandomDefault, len, &bytes)
|
|
if status != errSecSuccess {
|
|
// Fall back to arc4random which can't fail.
|
|
for i in 0..<len { bytes[i] = UInt8.random(in: 0...255) }
|
|
}
|
|
return ctx.newUint8Array(Data(bytes))
|
|
}
|
|
|
|
ctx.installBridgeFunction(name: "crypto_random_uuid") { _ in
|
|
return UUID().uuidString.lowercased()
|
|
}
|
|
|
|
ctx.installBridgeFunction(name: "crypto_hash") { args in
|
|
guard args.count >= 2,
|
|
let algo = args[0].toString(),
|
|
let data = args[1].toData() else {
|
|
return NSNull()
|
|
}
|
|
let out = Self.hash(algo: algo, data: data)
|
|
guard let out = out else { return NSNull() }
|
|
return ctx.newUint8Array(out)
|
|
}
|
|
|
|
ctx.installBridgeFunction(name: "crypto_hmac") { args in
|
|
guard args.count >= 3,
|
|
let algo = args[0].toString(),
|
|
let key = args[1].toData(),
|
|
let data = args[2].toData() else {
|
|
return NSNull()
|
|
}
|
|
let out = Self.hmac(algo: algo, key: key, data: data)
|
|
guard let out = out else { return NSNull() }
|
|
return ctx.newUint8Array(out)
|
|
}
|
|
|
|
ctx.installBridgeFunction(name: "crypto_pbkdf2") { args in
|
|
guard args.count >= 5,
|
|
let password = args[0].toData(),
|
|
let salt = args[1].toData(),
|
|
let iter = args[2].toNumber()?.uint32Value,
|
|
let keyLen = args[3].toNumber()?.intValue,
|
|
let digest = args[4].toString() else {
|
|
return NSNull()
|
|
}
|
|
let out = Self.pbkdf2(
|
|
password: password,
|
|
salt: salt,
|
|
iterations: iter,
|
|
keyLength: keyLen,
|
|
digest: digest
|
|
)
|
|
guard let out = out else { return NSNull() }
|
|
return ctx.newUint8Array(out)
|
|
}
|
|
|
|
ctx.installBridgeFunction(name: "crypto_aes_gcm_encrypt") { args in
|
|
guard args.count >= 3,
|
|
let key = args[0].toData(),
|
|
let nonce = args[1].toData(),
|
|
let plaintext = args[2].toData() else {
|
|
return NSNull()
|
|
}
|
|
let aad: Data? = args.count >= 4 ? args[3].toData() : nil
|
|
guard let sealed = Self.aesGcmEncrypt(key: key, nonce: nonce, plaintext: plaintext, aad: aad) else {
|
|
return NSNull()
|
|
}
|
|
return [
|
|
"ciphertext": ctx.newUint8Array(sealed.ciphertext),
|
|
"tag": ctx.newUint8Array(sealed.tag),
|
|
] as [String: Any]
|
|
}
|
|
|
|
ctx.installBridgeFunction(name: "crypto_aes_gcm_decrypt") { args in
|
|
guard args.count >= 4,
|
|
let key = args[0].toData(),
|
|
let nonce = args[1].toData(),
|
|
let ciphertext = args[2].toData(),
|
|
let tag = args[3].toData() else {
|
|
return NSNull()
|
|
}
|
|
let aad: Data? = args.count >= 5 ? args[4].toData() : nil
|
|
guard let plain = Self.aesGcmDecrypt(key: key, nonce: nonce, ciphertext: ciphertext, tag: tag, aad: aad) else {
|
|
return NSNull()
|
|
}
|
|
return ctx.newUint8Array(plain)
|
|
}
|
|
}
|
|
|
|
// MARK: - Hashes
|
|
|
|
static func hash(algo: String, data: Data) -> Data? {
|
|
switch algo.lowercased() {
|
|
case "sha256":
|
|
return Data(SHA256.hash(data: data))
|
|
case "sha512":
|
|
return Data(SHA512.hash(data: data))
|
|
case "sha1":
|
|
return Data(Insecure.SHA1.hash(data: data))
|
|
case "md5":
|
|
return Data(Insecure.MD5.hash(data: data))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func hmac(algo: String, key: Data, data: Data) -> Data? {
|
|
let symmetricKey = SymmetricKey(data: key)
|
|
switch algo.lowercased() {
|
|
case "sha256":
|
|
let mac = HMAC<SHA256>.authenticationCode(for: data, using: symmetricKey)
|
|
return Data(mac)
|
|
case "sha512":
|
|
let mac = HMAC<SHA512>.authenticationCode(for: data, using: symmetricKey)
|
|
return Data(mac)
|
|
case "sha1":
|
|
let mac = HMAC<Insecure.SHA1>.authenticationCode(for: data, using: symmetricKey)
|
|
return Data(mac)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func pbkdf2(
|
|
password: Data,
|
|
salt: Data,
|
|
iterations: UInt32,
|
|
keyLength: Int,
|
|
digest: String
|
|
) -> Data? {
|
|
let prf: CCPseudoRandomAlgorithm
|
|
switch digest.lowercased() {
|
|
case "sha256": prf = CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256)
|
|
case "sha512": prf = CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512)
|
|
case "sha1": prf = CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA1)
|
|
default: return nil
|
|
}
|
|
|
|
var derived = Data(count: keyLength)
|
|
let status = derived.withUnsafeMutableBytes { (derivedRaw: UnsafeMutableRawBufferPointer) -> Int32 in
|
|
guard let derivedBase = derivedRaw.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
|
return Int32(kCCParamError)
|
|
}
|
|
return password.withUnsafeBytes { (pwRaw: UnsafeRawBufferPointer) -> Int32 in
|
|
guard let pwBase = pwRaw.baseAddress?.assumingMemoryBound(to: Int8.self) else {
|
|
return Int32(kCCParamError)
|
|
}
|
|
return salt.withUnsafeBytes { (saltRaw: UnsafeRawBufferPointer) -> Int32 in
|
|
guard let saltBase = saltRaw.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
|
return Int32(kCCParamError)
|
|
}
|
|
return CCKeyDerivationPBKDF(
|
|
CCPBKDFAlgorithm(kCCPBKDF2),
|
|
pwBase, password.count,
|
|
saltBase, salt.count,
|
|
prf,
|
|
iterations,
|
|
derivedBase, keyLength
|
|
)
|
|
}
|
|
}
|
|
}
|
|
if status != 0 { return nil }
|
|
return derived
|
|
}
|
|
|
|
// MARK: - AES-GCM
|
|
|
|
struct SealedBlob {
|
|
let ciphertext: Data
|
|
let tag: Data
|
|
}
|
|
|
|
static func aesGcmEncrypt(key: Data, nonce: Data, plaintext: Data, aad: Data?) -> SealedBlob? {
|
|
guard key.count == 16 || key.count == 32 else { return nil }
|
|
guard nonce.count == 12 else { return nil }
|
|
let symmetric = SymmetricKey(data: key)
|
|
guard let aesNonce = try? AES.GCM.Nonce(data: nonce) else { return nil }
|
|
do {
|
|
let sealed: AES.GCM.SealedBox
|
|
if let aad = aad {
|
|
sealed = try AES.GCM.seal(plaintext, using: symmetric, nonce: aesNonce, authenticating: aad)
|
|
} else {
|
|
sealed = try AES.GCM.seal(plaintext, using: symmetric, nonce: aesNonce)
|
|
}
|
|
return SealedBlob(ciphertext: sealed.ciphertext, tag: sealed.tag)
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func aesGcmDecrypt(key: Data, nonce: Data, ciphertext: Data, tag: Data, aad: Data?) -> Data? {
|
|
guard key.count == 16 || key.count == 32 else { return nil }
|
|
guard nonce.count == 12 else { return nil }
|
|
let symmetric = SymmetricKey(data: key)
|
|
guard let aesNonce = try? AES.GCM.Nonce(data: nonce),
|
|
let sealed = try? AES.GCM.SealedBox(nonce: aesNonce, ciphertext: ciphertext, tag: tag) else {
|
|
return nil
|
|
}
|
|
do {
|
|
if let aad = aad {
|
|
return try AES.GCM.open(sealed, using: symmetric, authenticating: aad)
|
|
} else {
|
|
return try AES.GCM.open(sealed, using: symmetric)
|
|
}
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - JSValue numeric helpers used by the bridge
|
|
|
|
extension JSValue {
|
|
/// Returns a Swift NSNumber for numeric JSValues. Nil for non-numbers.
|
|
func toNumber() -> NSNumber? {
|
|
guard isNumber else { return nil }
|
|
return NSNumber(value: toDouble())
|
|
}
|
|
}
|