stablyai/orca · critical · Error
Invalid public key: expected 32 bytes, got ${key.length} fro
Error message
Invalid public key: expected 32 bytes, got ${key.length} from "${b64.slice(0, 20)}..." What it means
publicKeyFromBase64 decoded a base64 string whose byte length is not exactly 32 — the Curve25519 public key size required by tweetnacl's box.keyPair / box.before. The function base64-decodes via atob, builds a Uint8Array, and checks length. A wrong length means the pinned desktop public key is corrupt, truncated, or encoded with the wrong scheme.
Source
Thrown at mobile/src/transport/e2ee.ts:54
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]!)
}
return btoa(binary)
}
function base64ToUint8(b64: string): Uint8Array {
const binary = atob(b64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes
}
export function publicKeyFromBase64(b64: string): Uint8Array {
const key = base64ToUint8(b64)
if (key.length !== 32) {
throw new Error(
`Invalid public key: expected 32 bytes, got ${key.length} from "${b64.slice(0, 20)}..."`
)
}
return key
}
export function publicKeyToBase64(key: Uint8Array): string {
return uint8ToBase64(key)
}
export function encrypt(plaintext: string, sharedKey: Uint8Array): string {
const messageBytes = u8(new TextEncoder().encode(plaintext))
return uint8ToBase64(encryptBytes(messageBytes, sharedKey))
}
export function decrypt(encrypted: string, sharedKey: Uint8Array): string | null {
const bundle = base64ToUint8(encrypted)
const plaintext = decryptBytes(bundle, sharedKey)View on GitHub (pinned to 1136503c6a)
Solutions
- Trim whitespace and strip any data: prefix from the base64 string before decoding.
- Re-pair the host to get a fresh 32-byte (44 base64 chars) public key.
- Validate the base64 length before calling: 44 chars without padding = 32 bytes.
- If migrating from hex, convert hex→bytes first (64 hex chars = 32 bytes), then re-encode as base64.
Example fix
// before
export function publicKeyFromBase64(b64: string): Uint8Array {
const key = base64ToUint8(b64)
if (key.length !== 32) {
throw new Error(`Invalid public key: expected 32 bytes, got ${key.length} ...`)
}
return key
}
// after — sanitize + validate before decode
export function publicKeyFromBase64(b64: string): Uint8Array {
const clean = b64.trim().replace(/^data:.*?;base64,/, '')
const key = base64ToUint8(clean)
if (key.length !== 32) {
throw new Error(`Invalid public key: expected 32 bytes, got ${key.length}`)
}
return key
} Defensive patterns
Strategy: validation
Validate before calling
function isValidPublicKeyB64(b64: string): boolean {
try {
const clean = b64.trim().replace(/\s/g, '')
const bytes = Uint8Array.from(atob(clean), (c) => c.charCodeAt(0))
return bytes.length === 32
} catch {
return false
}
}
// Use before calling connect()
if (!isValidPublicKeyB64(serverPublicKeyB64)) {
throw new Error('Pairing key is corrupt — re-pair the host')
} Type guard
function isBase64Key32(b64: string): boolean {
try {
const bytes = Uint8Array.from(atob(b64.trim()), (c) => c.charCodeAt(0))
return bytes.length === 32
} catch {
return false
}
} Try / catch
try {
const client = connect(endpoint, deviceToken, serverPublicKeyB64, options)
} catch (e) {
if (e.message.startsWith('Invalid public key')) {
triggerRePair()
}
} Prevention
- Validate the base64 key length (44 chars no padding = 32 bytes) before pairing.
- Trim whitespace and strip data: prefixes from the key string.
- Re-pair if the stored publicKeyB64 in the host profile is corrupt.
- Do not pass hex-encoded keys — convert to base64 first.
When it happens
Trigger: The publicKeyB64 from pairing was truncated or padded wrong; a hex-encoded key was passed where base64 was expected; the key string has whitespace/newlines that atob mishandled; the stored host profile's publicKeyB64 was corrupted in AsyncStorage.
Common situations: Pairing QR/deeplink delivered a partial key; key copied with a stray newline or trailing '='; migration from a hex-key format; AsyncStorage corruption after an app crash mid-write; a relay key (16-byte) was passed instead of a Curve25519 key.
Related errors
- Invalid client nonce length: ${clientNonce.length}
- Invalid ${label}: expected ${expected} bytes, got ${bytes.le
- Invalid accounts snapshot from host
- Invalid reset response from host
- E2EE v2 ready has not been accepted
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/63dd6fb6a5e61599.
Report an issue: GitHub.