Private Search
Skiff implements client-side encrypted search that allows users to search their encrypted data without exposing plaintext content to servers. This is accomplished through local search indexing using MiniSearch and encrypted index storage.Overview
Private search in Skiff works by:- Building search indices locally on the client using MiniSearch
- Encrypting the entire search index before storage
- Storing encrypted indices in IndexedDB
- Decrypting indices locally when performing searches
- Never sending plaintext queries or content to servers
- Servers never see your search queries
- Servers never see what content matches your searches
- All indexing and searching happens client-side
- Search indices remain private even if stored data is compromised
MiniSearch Implementation
Skiff uses MiniSearch, a lightweight full-text search engine for JavaScript.Search Index Structure
The search index stores:libs/skiff-front-search/src/searchIndex.ts:16-20
Creating a Search Index
Search indices are created with specific options for the content type:libs/skiff-front-search/src/searchIndex.ts:24-36
Key features:
- Uses compressed JSON datagram for efficient storage
- Version constraint allows compatible updates
- Type parameter customizes for different search clients (email, documents, etc.)
- Metadata tracks indexing progress and state
Loading or Creating an Index
libs/skiff-front-search/src/searchIndex.ts:37-41
The constructor handles both cases:
libs/skiff-front-search/src/searchIndex.ts:54-83
Encryption Layer
Encrypting the Search Index
Search indices use a hybrid encryption approach:libs/skiff-front-search/src/encryption.ts:35-46
Why this approach?
- Symmetric encryption is fast for large search indices
- Asymmetric encryption secures the symmetric key
- User can decrypt with their private key
- No separate key management needed
Decrypting the Search Index
libs/skiff-front-search/src/encryption.ts:16-31
Decryption flow:
- Retrieve encrypted search data from IndexedDB
- Decrypt the symmetric key using the user’s private key
- Decrypt the search index using the symmetric key
- Load the decrypted index into MiniSearch
- Ready to search!
Index Operations
Adding Items to the Index
libs/skiff-front-search/src/searchIndex.ts:119-128
Removing Items from the Index
libs/skiff-front-search/src/searchIndex.ts:130-138
Searching the Index
libs/skiff-front-search/src/searchIndex.ts:149-193
Search features:
- Empty query: Returns recent items
- Fuzzy matching: Tolerates typos and misspellings
- Prefix matching: Matches partial words
- Date filtering: Filter by date ranges
- AND/OR logic: Prefer full matches, fall back to partial
- Sorting: Chronological or relevance-based
- Auto-suggest: Provides query suggestions
Saving the Index
The save operation is debounced for performance:libs/skiff-front-search/src/searchIndex.ts:85-108
Why debouncing?
- Reduces frequent encryption/storage operations
- Batches multiple updates together
- Ensures index is saved regularly (max 30 seconds)
- Calls
vacuum()to optimize index before saving
Index Maintenance
Checking Index Freshness
libs/skiff-front-search/src/searchIndex.ts:210-221
This identifies items that:
- Are not yet indexed
- Have been updated since indexing
Pruning Deleted Items
libs/skiff-front-search/src/searchIndex.ts:195-207
This removes items from the index that no longer exist in the source data.
Recent Items
When no search query is provided, show recently updated items:libs/skiff-front-search/src/searchIndex.ts:140-147
Privacy Guarantees
What Stays Private
- Search queries: Never sent to servers
- Search results: Computed entirely client-side
- Index content: Encrypted before storage
- Plaintext data: Never exposed outside the client
- User behavior: Search patterns not tracked
What Servers See
- Encrypted index data: Meaningless ciphertext
- IndexedDB operations: Normal browser storage
- Document retrieval: When you open a search result (but not the query)
Attack Scenarios
Scenario: Server compromise- Encrypted indices remain secure
- Attacker cannot decrypt without user’s private key
- No plaintext queries or results leaked
- Only encrypted data transmitted
- No search queries sent over network
- Search happens entirely offline
- If client is compromised, plaintext is accessible
- This is true for any client-side encryption
- Use endpoint security and secure devices
Performance Optimization
Compressed Storage
Search indices use gzip compression:libs/skiff-front-search/src/searchIndex.ts:30-34
This reduces storage size significantly for large indices.
Debounced Saves
As shown earlier, saves are debounced:- Wait 5 seconds after the last change
- Force save every 30 seconds maximum
- Reduces encryption overhead
Index Vacuum
Before saving, the index is vacuumed to remove fragmentation:libs/skiff-front-search/src/searchIndex.ts:87
This optimizes the index structure for smaller size and faster searches.
Memoization
Decryption results can be memoized to avoid repeated decryption:libs/skiff-crypto/src/asymmetricEncryption.ts:74-81
Implementation Example
Here’s a complete example of implementing private search:Best Practices
Index Design
- Choose indexed fields carefully: Only index searchable content
- Use field boosting: Prioritize important fields (titles, subjects)
- Store minimal fields: Don’t store entire documents, just IDs and snippets
- Update incrementally: Add/remove items as they change
Performance
- Batch updates: Add multiple items before saving
- Prune regularly: Remove deleted items from the index
- Vacuum periodically: Optimize index structure
- Use compression: Enable for large indices
- Lazy load: Don’t decrypt index until needed
Security
- Protect private keys: Never expose user’s private key
- Validate inputs: Sanitize search queries and indexed content
- Clear on logout: Remove decrypted indices from memory
- Monitor index size: Prevent DoS through index bloat
- Use HTTPS: Protect data in transit
User Experience
- Show recent items: Display recent docs when query is empty
- Provide feedback: Show indexing progress
- Handle errors gracefully: Rebuild corrupt indices
- Auto-suggest: Help users find content faster
- Highlight matches: Show why results matched
Limitations
What Private Search Can’t Do
- Server-side ranking: All ranking must be client-side
- Aggregations: Can’t aggregate across users’ data
- Global search: Each user’s index is separate
- Instant updates: New content requires re-indexing
- Cross-device sync: Each device maintains its own index
Performance Considerations
- Index size: Large indices take longer to encrypt/decrypt
- Indexing time: Building initial index can be slow
- Memory usage: Index must fit in browser memory
- Battery impact: Encryption/decryption uses CPU
- Storage limits: IndexedDB has size limits (varies by browser)
Troubleshooting
Index Not Saving
Problem: Changes to index aren’t persisted Solutions:- Check IndexedDB quota (may be full)
- Ensure save debounce isn’t preventing saves
- Call
save.flush()to force immediate save - Check browser console for errors
Search Not Working
Problem: Queries return no results Solutions:- Verify items are actually indexed (
searchIndex.isIndexed(item)) - Check search field configuration in options
- Try broader search options (more fuzzy, prefix matching)
- Ensure indexed fields contain searchable text
Performance Issues
Problem: Search is slow or freezing browser Solutions:- Reduce index size (fewer fields, prune old items)
- Enable compression in datagram
- Use Web Workers for indexing (not shown here)
- Limit search result count
- Optimize MiniSearch options
Decryption Failures
Problem: Can’t load encrypted index Solutions:- Verify user keys are correct
- Check datagram version constraints
- Clear corrupt index and rebuild
- Check browser console for specific error
Next Steps
Encryption
Learn about the encryption methods used for search indices
Key Management
Understand how to manage cryptographic keys