77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
// Why: Orca's remote runtime transports share one NaCl box format across
|
|
// desktop, CLI, and mobile pairing. Keeping the Node-compatible primitives in
|
|
// shared code prevents the CLI from importing main-process modules.
|
|
import nacl from 'tweetnacl'
|
|
|
|
const MAX_E2EE_TEXT_PLAINTEXT_BYTES = 4 * 1024 * 1024
|
|
const LEGACY_E2EE_FRAME_OVERHEAD_BYTES = nacl.box.nonceLength + nacl.box.overheadLength
|
|
export const MAX_E2EE_ENCRYPTED_BASE64_CHARACTERS =
|
|
Math.ceil((MAX_E2EE_TEXT_PLAINTEXT_BYTES + LEGACY_E2EE_FRAME_OVERHEAD_BYTES) / 3) * 4
|
|
|
|
export function generateKeyPair(): nacl.BoxKeyPair {
|
|
return nacl.box.keyPair()
|
|
}
|
|
|
|
export function deriveSharedKey(ourSecretKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array {
|
|
return nacl.box.before(peerPublicKey, ourSecretKey)
|
|
}
|
|
|
|
export function publicKeyFromBase64(b64: string): Uint8Array {
|
|
if (b64.length > 44) {
|
|
throw new Error('Invalid public key: encoded value is too large')
|
|
}
|
|
const key = Uint8Array.from(Buffer.from(b64, 'base64'))
|
|
if (key.length !== 32) {
|
|
throw new Error(`Invalid public key: expected 32 bytes, got ${key.length}`)
|
|
}
|
|
return key
|
|
}
|
|
|
|
export function publicKeyToBase64(key: Uint8Array): string {
|
|
return Buffer.from(key).toString('base64')
|
|
}
|
|
|
|
export function encrypt(plaintext: string, sharedKey: Uint8Array): string {
|
|
const messageBytes = new TextEncoder().encode(plaintext)
|
|
return Buffer.from(encryptBytes(messageBytes, sharedKey)).toString('base64')
|
|
}
|
|
|
|
export function decrypt(encrypted: string, sharedKey: Uint8Array): string | null {
|
|
if (encrypted.length > MAX_E2EE_ENCRYPTED_BASE64_CHARACTERS) {
|
|
return null
|
|
}
|
|
const bundle = Uint8Array.from(Buffer.from(encrypted, 'base64'))
|
|
const plaintext = decryptBytes(bundle, sharedKey)
|
|
return plaintext ? new TextDecoder().decode(plaintext) : null
|
|
}
|
|
|
|
export function encryptBytes(
|
|
plaintext: Uint8Array<ArrayBufferLike>,
|
|
sharedKey: Uint8Array
|
|
): Uint8Array {
|
|
const nonce = nacl.randomBytes(nacl.box.nonceLength)
|
|
const ciphertext = nacl.box.after(plaintext, nonce, sharedKey)
|
|
|
|
const bundle = new Uint8Array(nonce.length + ciphertext.length)
|
|
bundle.set(nonce)
|
|
bundle.set(ciphertext, nonce.length)
|
|
|
|
return bundle
|
|
}
|
|
|
|
export function decryptBytes(bundle: Uint8Array, sharedKey: Uint8Array): Uint8Array | null {
|
|
if (bundle.length < nacl.box.nonceLength + nacl.box.overheadLength) {
|
|
return null
|
|
}
|
|
|
|
const nonce = bundle.slice(0, nacl.box.nonceLength)
|
|
const ciphertext = bundle.slice(nacl.box.nonceLength)
|
|
const plaintext = nacl.box.open.after(ciphertext, nonce, sharedKey)
|
|
|
|
if (!plaintext) {
|
|
return null
|
|
}
|
|
|
|
return plaintext
|
|
}
|