-
Notifications
You must be signed in to change notification settings - Fork 20
feat(sync): optional end-to-end encryption for cloud sync and backups (#520) #548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
61986a3
docs: encrypted cloud sync (E2E) design spec (#520)
ericgriffin 6fad1bd
docs: encrypted cloud sync implementation plan + cryptography deps (#…
ericgriffin fa4ca19
feat(sync): SBE1 encrypted envelope with python-generated KAT vectors…
ericgriffin 84f1f72
feat(sync): keyslot file with Argon2id passphrase/recovery wrapping (…
ericgriffin 1d8ae4c
feat(sync): recovery code generation on the EFF short wordlist (#520)
ericgriffin a67d62c
feat(sync): local key custody, keyslot mirror, encryption preference …
ericgriffin 994ece0
feat(sync): encrypting cloud storage decorator (#520)
ericgriffin ecf144d
feat(sync): awaitingPassphrase halt for encrypted libraries (#520)
ericgriffin 7962986
feat(sync): encryption lifecycle service - enable, unlock, rotate slo…
ericgriffin 344eeb9
feat(sync): riverpod wiring for encryption session and provider wrap …
ericgriffin 50014ff
feat(backup): framed self-decrypting .sbe backup encryption (#520)
ericgriffin 99940b1
feat(backup): encrypted cloud backup upload, restore, plaintext clean…
ericgriffin a39592f
test(backup): update restore-service doubles for the encryptionSecret…
ericgriffin 30da9be
test(sync): no-plaintext-leak invariant and encrypted two-device conv…
ericgriffin 6afdf1e
feat(settings): end-to-end encryption section with enable and manage …
ericgriffin 1823276
feat(sync): unlock banner, troubleshoot status, encrypted restore pro…
ericgriffin 09c56d3
l10n: translate sync encryption strings into all locales (#520)
ericgriffin 2fa8031
fix(sync): address PR #548 review + raise patch coverage to 97% (#520)
ericgriffin 960d133
docs: v1.6.1.115 release notes for encrypted cloud sync (#520)
ericgriffin fad12de
fix(sync): address PR #548 re-review comments (#520)
ericgriffin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
2,476 changes: 2,476 additions & 0 deletions
2,476
docs/superpowers/plans/2026-07-10-encrypted-cloud-sync.md
Large diffs are not rendered by default.
Oops, something went wrong.
403 changes: 403 additions & 0 deletions
403
docs/superpowers/specs/2026-07-10-encrypted-cloud-sync-design.md
Large diffs are not rendered by default.
Oops, something went wrong.
119 changes: 119 additions & 0 deletions
119
lib/core/services/cloud_storage/encrypting_cloud_storage_provider.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import 'dart:typed_data'; | ||
|
|
||
| import 'package:cryptography/cryptography.dart'; | ||
|
|
||
| import 'package:submersion/core/services/cloud_storage/cloud_storage_provider.dart'; | ||
| import 'package:submersion/core/services/sync/crypto/crypto_errors.dart'; | ||
| import 'package:submersion/core/services/sync/crypto/keyslots.dart'; | ||
| import 'package:submersion/core/services/sync/crypto/sync_envelope.dart'; | ||
|
|
||
| /// Wraps the configured provider so every non-exempt byte stored in the | ||
| /// cloud is an SBE1 envelope. Filenames, listing, folders, and auth pass | ||
| /// through untouched; encryption is invisible to readers and writers above | ||
| /// this seam (spec section 4.1). | ||
| class EncryptingCloudStorageProvider implements CloudStorageProvider { | ||
| final CloudStorageProvider inner; | ||
| final SecretKey _dataKey; | ||
| final String _libraryKeyId; | ||
|
|
||
| /// fileId -> filename, populated from list/info/upload results so | ||
| /// downloads know the AAD. Misses fall back to [getFileInfo]. | ||
| final Map<String, String> _names = {}; | ||
|
|
||
| EncryptingCloudStorageProvider( | ||
| this.inner, { | ||
| required SecretKey dataKey, | ||
| required String libraryKeyId, | ||
| }) : _dataKey = dataKey, | ||
| _libraryKeyId = libraryKeyId; | ||
|
|
||
| /// Exactly two exemptions (spec 4.1): the keyslot bootstrap file and | ||
| /// backup artifacts, which carry their own framed encryption. | ||
| static bool isExempt(String filename) => | ||
| filename == KeyslotFile.cloudFileName || | ||
| filename.startsWith('submersion_backup_'); | ||
|
|
||
| @override | ||
| Future<UploadResult> uploadFile( | ||
| Uint8List data, | ||
| String filename, { | ||
| String? folderId, | ||
| }) async { | ||
| final bytes = isExempt(filename) | ||
| ? data | ||
| : await SyncEnvelope.seal( | ||
| plaintext: data, | ||
| dataKey: _dataKey, | ||
| libraryKeyId: _libraryKeyId, | ||
| filename: filename, | ||
| ); | ||
| final result = await inner.uploadFile(bytes, filename, folderId: folderId); | ||
| _names[result.fileId] = filename; | ||
| return result; | ||
| } | ||
|
|
||
| @override | ||
| Future<Uint8List> downloadFile(String fileId) async { | ||
| final bytes = await inner.downloadFile(fileId); | ||
| if (!SyncEnvelope.hasMagic(bytes)) return bytes; | ||
| final name = _names[fileId] ?? (await inner.getFileInfo(fileId))?.name; | ||
| if (name == null) { | ||
| throw const EnvelopeCorruptException( | ||
| 'Encrypted file has no resolvable name for authentication', | ||
| ); | ||
| } | ||
| _names[fileId] = name; | ||
| return SyncEnvelope.open( | ||
| envelope: bytes, | ||
| dataKey: _dataKey, | ||
| expectedLibraryKeyId: _libraryKeyId, | ||
| filename: name, | ||
| ); | ||
| } | ||
|
ericgriffin marked this conversation as resolved.
|
||
|
|
||
| @override | ||
| Future<List<CloudFileInfo>> listFiles({ | ||
| String? folderId, | ||
| String? namePattern, | ||
| }) async { | ||
| final files = await inner.listFiles( | ||
| folderId: folderId, | ||
| namePattern: namePattern, | ||
| ); | ||
| for (final f in files) { | ||
| _names[f.id] = f.name; | ||
| } | ||
| return files; | ||
| } | ||
|
|
||
| @override | ||
| Future<CloudFileInfo?> getFileInfo(String fileId) async { | ||
| final info = await inner.getFileInfo(fileId); | ||
| if (info != null) _names[info.id] = info.name; | ||
| return info; | ||
| } | ||
|
|
||
| @override | ||
| String get providerName => inner.providerName; | ||
| @override | ||
| String get providerId => inner.providerId; | ||
| @override | ||
| Future<bool> isAvailable() => inner.isAvailable(); | ||
| @override | ||
| Future<bool> isAuthenticated() => inner.isAuthenticated(); | ||
| @override | ||
| Future<void> authenticate() => inner.authenticate(); | ||
| @override | ||
| Future<void> signOut() => inner.signOut(); | ||
| @override | ||
| Future<String?> getUserEmail() => inner.getUserEmail(); | ||
| @override | ||
| Future<void> deleteFile(String fileId) => inner.deleteFile(fileId); | ||
| @override | ||
| Future<bool> fileExists(String fileId) => inner.fileExists(fileId); | ||
| @override | ||
| Future<String> createFolder(String folderName, {String? parentFolderId}) => | ||
| inner.createFolder(folderName, parentFolderId: parentFolderId); | ||
| @override | ||
| Future<String> getOrCreateSyncFolder() => inner.getOrCreateSyncFolder(); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /// The cloud library is encrypted and this device holds no (matching) key. | ||
| /// | ||
| /// Carriers of this exception halt sync with | ||
| /// SyncResultStatus.awaitingPassphrase instead of surfacing a raw error. | ||
| class SyncEncryptionRequired implements Exception { | ||
| final String? libraryKeyId; | ||
| final String message; | ||
|
|
||
| const SyncEncryptionRequired({ | ||
| this.libraryKeyId, | ||
| this.message = 'The cloud library is encrypted', | ||
| }); | ||
|
|
||
| @override | ||
| String toString() => 'SyncEncryptionRequired($libraryKeyId): $message'; | ||
| } | ||
|
|
||
| /// An SBE1 envelope failed structural parsing or authentication. Treated | ||
| /// like a checksum failure: transient-stop, never a silent fallback. | ||
| class EnvelopeCorruptException implements Exception { | ||
| final String message; | ||
|
|
||
| const EnvelopeCorruptException(this.message); | ||
|
|
||
| @override | ||
| String toString() => 'EnvelopeCorruptException: $message'; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.