Skip to main content

Secure Transport

End-to-end encrypted communication using elliptic curve Diffie-Hellman key exchange, symmetric encryption, and digital signatures — all via CryptoKit.

Key Agreement

P256 ECDH key exchange with HKDF-SHA256 key derivation.
let alice = PrismKeyAgreement()
let bob = PrismKeyAgreement()

// Exchange public keys (64 bytes, P256 raw)
let alicePublic = alice.publicKeyData
let bobPublic = bob.publicKeyData

// Derive shared secret — same on both sides
let aliceKey = try alice.deriveSharedSecret(with: bobPublic)
let bobKey = try bob.deriveSharedSecret(with: alicePublic)
// aliceKey == bobKey ✓

Custom Salt

let key = try alice.deriveSharedSecret(
    with: bobPublic,
    salt: Data("session-2024".utf8)
)
Different salts produce different derived keys from the same shared secret. Use unique salts for different purposes (encryption key, MAC key, etc.).

Secure Envelope

Encrypt-then-sign format with ephemeral keys for forward secrecy. Each message uses a fresh ECDH key pair — compromising the sender’s long-term key doesn’t decrypt past messages.

Seal and Open

import CryptoKit

let senderSigning = P256.Signing.PrivateKey()
let recipientAgreement = P256.KeyAgreement.PrivateKey()

// Sender seals
let envelope = try PrismSecureEnvelope.seal(
    data: Data("Top secret".utf8),
    recipientPublicKey: recipientAgreement.publicKey.rawRepresentation,
    senderSigningKey: senderSigning
)

// Recipient opens (verifies signature + decrypts)
let plaintext = try PrismSecureEnvelope.open(
    envelope,
    recipientPrivateKey: recipientAgreement,
    senderVerifyKey: senderSigning.publicKey.rawRepresentation
)

Codable Values

struct SecureMessage: Codable, Sendable, Equatable {
    let text: String
    let priority: Int
}

// Seal a Codable value
let envelope = try PrismSecureEnvelope.seal(
    SecureMessage(text: "urgent", priority: 1),
    recipientPublicKey: recipientPublic,
    senderSigningKey: senderKey
)

// Open into typed value
let message = try PrismSecureEnvelope.open(
    SecureMessage.self,
    from: envelope,
    recipientPrivateKey: recipientKey,
    senderVerifyKey: senderPublic
)

Envelope Structure

FieldDescription
ephemeralPublicKeyFresh P256 key (forward secrecy)
ciphertextAES-GCM encrypted payload
signatureP256 ECDSA over ciphertext + ephemeral key
createdAtTimestamp
Sender:
  1. Generate ephemeral P256 key pair
  2. ECDH(ephemeral, recipient) → shared secret
  3. HKDF(shared secret) → AES key
  4. AES-GCM.encrypt(plaintext, key) → ciphertext
  5. ECDSA.sign(ciphertext + ephemeral, sender signing key) → signature
  6. Envelope = { ephemeral pub, ciphertext, signature }

Recipient:
  1. ECDSA.verify(ciphertext + ephemeral, sender verify key)
  2. ECDH(recipient private, ephemeral) → shared secret
  3. HKDF(shared secret) → AES key
  4. AES-GCM.decrypt(ciphertext, key) → plaintext

Wrong Sender Key Detection

let imposter = P256.Signing.PrivateKey()

// Fails — signature doesn't match
do {
    try PrismSecureEnvelope.open(
        envelope,
        recipientPrivateKey: recipientKey,
        senderVerifyKey: imposter.publicKey.rawRepresentation
    )
} catch {
    // PrismSecurityError — signature verification failed
}

Secure Channel

Bidirectional encrypted pipe between two parties. Establish once, encrypt/decrypt many times.

Basic Usage

let alice = PrismSecureChannel()
let bob = PrismSecureChannel()

// Establish (both sides)
try alice.establish(with: bob.publicKeyData)
try bob.establish(with: alice.publicKeyData)

// Encrypt / Decrypt
let encrypted = try alice.encrypt(Data("Hello Bob".utf8))
let decrypted = try bob.decrypt(encrypted)

Codable Through Channel

struct Payload: Codable, Sendable, Equatable {
    let id: Int
    let name: String
}

let encrypted = try alice.encrypt(Payload(id: 42, name: "test"))
let decoded = try bob.decrypt(Payload.self, from: encrypted)

Algorithm Selection

// Default: AES-GCM
let channel = PrismSecureChannel(algorithm: .aesGCM)

// Alternative: ChaChaPoly (faster on devices without AES-NI)
let channel = PrismSecureChannel(algorithm: .chaChaPoly)

Lifecycle

let channel = PrismSecureChannel()

channel.isEstablished  // false

try channel.establish(with: remotePublicKey)
channel.isEstablished  // true

// Use channel...

channel.close()        // zeros shared key
channel.isEstablished  // false

// Throws PrismSecurityError.invalidKey
try channel.encrypt(data)
PrismSecureChannel uses NSLock for thread safety — safe to call from multiple threads. It’s Sendable and works across actor boundaries.