Deck's Privacy Architecture: How a Clipboard Manager Protects What It Reads
Defense in depth — sensitive content filtering, AES-GCM encryption, Touch ID biometrics, and zero-width character steganography.
A clipboard manager sees everything the user copies — passwords, bank card numbers, ID numbers, confidential documents. Deck is a macOS-native clipboard manager built with Swift and SwiftUI, and this visibility comes with an obligation: the app must protect every piece of data it touches.
Deck's approach is defense in depth — multiple overlapping layers of protection covering filtering, encryption, access control, and covert communication.
Threat Model
Deck's security design targets three scenarios: (1) data leakage — sensitive content like passwords or card numbers accidentally saved to clipboard history; (2) local unauthorized access — someone with physical access to an unlocked Mac browsing the clipboard database; (3) in-transit interception — clipboard data exposed during LAN sharing or when pasted into untrusted channels. Each layer in the architecture maps to one or more of these threats.
1. The Sensitive Content Filtering Pipeline
When a user copies a bank card number from a chat window, the filtering pipeline intercepts it within milliseconds — before the number touches the database.
The pipeline lives in ClipboardService.swift, triggered inside checkForChangesInternal() whenever a clipboard change is detected. It runs four checks in sequence, ordered from fastest to slowest so the cheapest checks act as early exits.
flowchart TD
A["`Clipboard Changed\n剪贴板变化`"] --> B{"`System Password\nType Marker?\n系统密码类型标记?`"}
B -->|Yes 是| SKIP["`Skip — Record Nothing\n跳过,不记录`"]
B -->|No 否| C{"`Sensitive Window\nTitle?\n敏感窗口标题?`"}
C -->|Yes 是| SKIP
C -->|No 否| D{"`Bank Card\nNumber?\n银行卡号?`"}
D -->|Yes 是| SKIP
D -->|No 否| E{"`Identity Document\nNumber?\n身份证件号?`"}
E -->|Yes 是| SKIP
E -->|No 否| F["`Record Item\n记录条目`"]Layer 1: System Password Type Markers
The fastest check — zero parsing required. Password managers like 1Password and the macOS Keychain tag their clipboard entries with special pasteboard types:
org.nspasteboard.ConcealedType(community standardcom.apple.passwordcom.apple.securetext
If any of these types appear in pasteboard.types, Deck immediately skips the entry. No content is read, no content is stored.
Layer 2: Window Title-Aware Auto-Pause
Not all apps tag their sensitive content. When the user is typing in a login form, the window title often contains telltale words. Deck uses the macOS Accessibility API (AXUIElementCopyAttributeValue) to read the focused window's title and checks it against a curated list of ~20 keywords:
password, passwd, 密码, login, signin, 登录, credential, token,
secret, keychain, 钥匙串, vault, 2fa, otp, authenticator,
验证码, private, secure, 私密, 加密To avoid hammering the AX API on every poll cycle, window titles are cached by PID with a 0.4-second TTL. This check requires AXIsProcessTrusted() accessibility permission and is controlled by the smartWindowTitleDetection preference.
Layer 3: Bank Card Detection via Luhn Algorithm
Raw clipboard text is checked for sequences of 13–19 digits (ignoring spaces and hyphens), then validated in two stages:
Stage 1 — IIN/BIN prefix matching against known card networks:
| Prefix / 前缀 | Network / 卡组织 |
|---|---|
| 4 | Visa |
| 51–55 | Mastercard |
| 34, 37 | American Express |
| 62 | UnionPay / 银联 |
| 6011, 65 | Discover |
| 35 | JCB |
Stage 2 — only if the prefix matches, the Luhn checksum is computed:
func passesLuhnCheck(_ digits: String) -> Bool {
var sum = 0
for (index, digit) in digits.reversed().enumerated() {
if index % 2 == 1 {
let doubled = digit * 2
sum += doubled > 9 ? doubled - 9 : doubled
} else {
sum += digit
}
}
return sum % 10 == 0
}The two-stage design is deliberate: running Luhn on every digit sequence would waste cycles. The prefix check acts as a cheap filter, and Luhn only runs on plausible candidates.
Layer 4: Multi-Country Identity Document Detection
The final filter in the pipeline covers identity documents across multiple jurisdictions. Each sub-detector uses regex matching followed by country-specific checksum validation:
| Document / 证件类型 | Format / 格式 | Validation / 验证方式 |
|---|---|---|
| China Mainland 18-digit / 中国大陆 18 位 | \d{17}[\dXx] |
Weighted checksum / 加权校验码 |
| China Mainland 15-digit / 中国大陆 15 位 | \d{15} |
Regex / 正则 |
| Taiwan ID / 台湾身份证 | [A-Z][12]\d{8} |
Letter-mapped weighted check / 字母映射加权 |
| Hong Kong ID / 香港身份证 | [A-Z]{1,2}\d{6}\(?[0-9A]\)? |
Letter-value weighted / 字母值加权 |
| US SSN/ITIN / 美国 SSN/ITIN | \d{3}-\d{2}-\d{4} |
Area/group/serial rules / 区域规则 |
| German Tax ID / 德国税号 | 11 digits / 11 位 | Modulo 11 / 模 11 校验 |
| Passport / 护照号 | Multi-format / 多国格式 | Pattern matching / 模式匹配 |
Every sub-detector goes beyond regex. A string that looks like a China Mainland ID but fails the weighted checksum? It passes through. This prevents false positives from blocking legitimate content like order numbers or timestamps.
2. AES-GCM Full-Chain Encryption
The filtering pipeline handles known sensitive patterns, but ordinary clipboard content — notes, code snippets, personal messages — also needs protection at rest. Deck encrypts the entire data chain with AES-256-GCM, the same authenticated encryption standard used in TLS 1.3.
Key Management
A 256-bit symmetric key is generated on first use and stored in the macOS Keychain via getOrCreateEncryptionKey(). The key never touches the filesystem. To reduce Keychain access overhead, the key is cached in memory for 60 seconds.
Column-Level Database Encryption
When Security Mode is enabled (securityModeEnabled), the following database columns are encrypted individually before writing to SQLite:
data— raw clipboard bytessearch_text— searchable textapp_name— source applicationcustom_title— user-assigned titleembedding— semantic vector
// Encryption / 加密
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined // nonce + ciphertext + tag
// Decryption / 解密
let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
return try AES.GCM.open(sealedBox, using: key)Each call to AES.GCM.seal generates a fresh random nonce, so encrypting the same plaintext twice produces different ciphertext. The GCM authentication tag ensures any tampering with the ciphertext is detected on decryption.
Blob File Encryption
Clipboard items larger than 512 KB (largeBlobThreshold) are stored as standalone files rather than inline in the database. In Security Mode, these blob files are also AES-GCM encrypted before being written to disk.
LAN Sharing Encryption
When Deck syncs clipboard content between devices via MultipeerConnectivity, the shared data is encrypted with a session key negotiated after TOTP verification, using the same CryptoKit AES.GCM primitives as the rest of the encryption chain.
Performance Cost
AES-GCM on Apple Silicon is hardware-accelerated, so the per-item encryption overhead is negligible for typical clipboard entries (text, small images). The main cost shows up elsewhere: encrypted columns break SQLite FTS5 and vector indexes, forcing search to fall back to in-memory decryption-then-match (covered in Section 6). The 60-second in-memory key cache and the 300-entry searchTextCache exist specifically to keep this fallback path responsive.
3. Touch ID Biometric Authentication
Encryption covers data at rest and in transit, but it doesn't help if someone walks up to an unlocked Mac and opens Deck directly. Biometric authentication closes that gap with a physical identity check.
Deck uses the LocalAuthentication framework with a two-tier policy:
- Primary / 首选:
.deviceOwnerAuthenticationWithBiometrics— Touch ID, Face ID, or Optic ID - **Fallback
Two timeout modes are available:
- **Auto-lock
- **Every-time
4. Zero-Width Character Steganography
The layers above protect data inside Deck itself. Steganography extends that protection outward — it lets users hide messages inside ordinary-looking text before sharing, so the content stays concealed even if the transmission channel is compromised.
Zero-width characters are Unicode code points that occupy no visible space. Deck uses four of them to encode data at 2 bits per character:
| Character / 字符 | Code Point / 码位 | Bits / 位值 |
|---|---|---|
| ZWSP (Zero-Width Space / 零宽空格) | U+200B | 00 |
| ZWNJ (Zero-Width Non-Joiner / 零宽非连接符) | U+200C | 01 |
| ZWJ (Zero-Width Joiner / 零宽连接符) | U+200D | 10 |
| BOM (Byte Order Mark / 字节序标记) | U+FEFF | 11 |
The encoding pipeline:
Plaintext → AES-GCM Encrypt → Map Each 2-bit Pair to Zero-Width Char
→ Prepend Magic Header "DECKSTG1" → Embed into Cover Text
明文 → AES-GCM 加密 → 每 2 bit 映射为零宽字符
→ 添加魔数头部 "DECKSTG1" → 嵌入载体文本The result is a piece of text that looks completely normal — "Meeting moved to 3pm" — but contains an invisible, encrypted payload that only Deck can extract.
5. Image LSB Steganography
For richer payloads or when text isn't the right carrier, Deck supports Least Significant Bit (LSB) steganography in images.
The algorithm modifies the lowest bit of each RGB channel in every non-transparent pixel, yielding 3 bits of storage per pixel. A 1920×1080 image with no transparency can carry roughly 760 KB of hidden data — more than enough for most messages.
The pipeline mirrors the zero-width approach:
- Plaintext → AES-GCM encryption
- Prepend
DECKSTG1magic header (14 bytes, including payload length) - Write encrypted bits into the LSBs of pixel RGB channels
- Re-encode the image via
CGContext
Transparent pixels (alpha < 1) are skipped — modifying them would create visible artifacts. The carrier image limit is approximately 25 MB.
Steganography Key Management
Steganographic encryption uses a dedicated key, derived from the master key via HKDF-SHA256 (SteganographyKeyStore.swift). This separation ensures that compromising one key doesn't compromise the other.
6. The Cost of Security: Search Degradation in Security Mode
Security always has trade-offs. When Security Mode encrypts database columns, several search capabilities lose access to plaintext and must degrade gracefully:
| Search Layer / 搜索层 | Normal Mode / 正常模式 | Security Mode / 安全模式 |
|---|---|---|
| FTS5 Full-Text Search / FTS5 全文检索 | Available / 可用 | Disabled — search_text encrypted / 索引失效 |
| sqlite-vec Vector Search / sqlite-vec 向量检索 | Available / 可用 | Disabled — vecIndexEnabled = false |
| Semantic Search / 语义搜索 | NLEmbedding | Degraded to plain-text matching / 降级为纯文本匹配 |
| Fuzzy Search / 模糊搜索 | FuseSearch | Available — in-memory after decryption / 内存解密后搜索 |
| SQL LIKE | Available / 可用 | Disabled — columns encrypted / 列已加密 |
| In-Memory Search / 内存搜索 | Fallback / 备用 | Primary — searchWithLike / 主力 |
In Security Mode, search falls back to an in-memory scan: items are decrypted on-the-fly and matched in memory. To keep this practical, the scan is capped at max(5000, limit × 200) items (hard ceiling: 20,000), and a searchTextCache holds the 300 most recently decrypted texts to avoid redundant decryption.
It's not as fast as FTS5, but it preserves functionality without ever writing plaintext search indexes to disk.
7. How the Layers Fit Together
Each layer addresses a different threat surface. Filtering keeps sensitive data out of the system; encryption protects what does get stored; biometrics prevents unauthorized physical access; steganography protects data after it leaves Deck. None of them is sufficient on its own — the value is in the overlap.
graph LR
A["`Clipboard Input\n剪贴板输入`"] --> B["`Sensitive Content\nFiltering\n敏感内容过滤`"]
B --> C["`AES-GCM\nEncryption\nAES-GCM 加密`"]
C --> D["`Keychain Key\nManagement\n钥匙串密钥管理`"]
C --> E["`Blob File\nEncryption\nBlob 文件加密`"]
C --> F["`LAN Sharing\nEncryption\n局域网共享加密`"]
A --> G["`Touch ID\nBiometrics\n生物识别`"]
A --> H["`Steganography\n隐写术`"]
H --> H1["`Zero-Width\nChars\n零宽字符`"]
H --> H2["`Image LSB\n图片 LSB`"]- Filtering intercepts passwords, card numbers, and ID numbers at the entry point, keeping them out of storage entirely.
- Encryption covers database columns, on-disk blob files, and LAN transfers — protecting all stored and in-transit data.
- Biometrics requires Touch ID or a password before anyone can view clipboard history on an unlocked Mac.
- Steganography adds an invisible encryption layer for users sharing content through untrusted channels.
- When Security Mode is on, search automatically degrades to an in-memory scan, avoiding plaintext indexes on disk.
Deck reads everything the user copies. The architecture above makes sure that visibility doesn't become a liability — each layer reduces a different category of risk, and together they cover the gaps that any single mechanism would leave open.