Skip to content

Commit 2f3eea4

Browse files
committed
Preserve legacy archive credentials on migration failure and document audit coverage
1 parent 28db40c commit 2f3eea4

7 files changed

Lines changed: 18927 additions & 2 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ jobs:
6767
run: |
6868
python3 Tests/ArchiveLock/run.py
6969
python3 Tests/ArchiveLock/password_flow.py
70+
python3 Tests/ArchiveLock/migration.py
7071
- name: Managed proxy observation and customization tests
7172
run: |
7273
python3 Tests/ManagedProxy/run.py

‎.github/workflows/client-regression.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ jobs:
3636
- run: python3 Tests/Customization/run.py
3737
- run: python3 Tests/MenuPresentation/run.py
3838
- run: python3 Tests/ArchiveLock/password_flow.py
39+
- run: python3 Tests/ArchiveLock/migration.py
3940
- run: python3 Tests/NetworkPolicies/run.py
4041
- run: python3 Tests/NetworkRecovery/run.py

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
## [Unreleased]
88

99
### Fixed — September 27 audit
10+
- Failed legacy Archive password migration preserves the only credential; successful migration updates the notification-protection flag before discarding the legacy hash.
1011
- Archive password setup, rotation and removal no longer report success or change preference mirrors after a failed Keychain mutation. Password prompts release their action closures on dismissal.
1112
- Rejected short download replies no longer publish a false resource size to the media cache.
1213
- Multiline settings rows and fields respect asymmetric safe-area insets; actionable multiline rows expose a VoiceOver activation and a minimum 44-point row height.

‎Tests/ArchiveLock/migration.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Fault-injection check for the production settings-screen migration commit."""
2+
import pathlib
3+
import subprocess
4+
import tempfile
5+
6+
root = pathlib.Path(__file__).resolve().parents[2]
7+
source = (root / 'submodules/SettingsUI/Sources/ArchiveSettingsController.swift').read_text(encoding='utf-8')
8+
start = source.index(' if archiveSettings.legacyLockPasswordHash != nil')
9+
end = source.index('\n\n let controllerState', start)
10+
fixtures = '''
11+
import Foundation
12+
struct Settings {
13+
var legacyLockPasswordHash: String?
14+
var isPasswordConfigured: Bool
15+
func clearingLegacyPasswordHash() -> Settings {
16+
var value = self; value.legacyLockPasswordHash = nil; return value
17+
}
18+
func withUpdatedIsPasswordConfigured(_ configured: Bool) -> Settings {
19+
var value = self; value.isPasswordConfigured = configured; return value
20+
}
21+
}
22+
final class Engine {
23+
var settings: Settings
24+
init(_ settings: Settings) { self.settings = settings }
25+
}
26+
struct Account { let peerId = 1 }
27+
struct Context { let account = Account(); let engine: Engine }
28+
struct ResultSignal { func startStandalone() -> Int { 0 } }
29+
func updateChatArchiveSettings(engine: Engine, _ f: (Settings) -> Settings) -> ResultSignal {
30+
engine.settings = f(engine.settings); return ResultSignal()
31+
}
32+
enum ArchivePasswordKeychain {
33+
static var stored = false
34+
static func hasPassword(peerId: Int) -> Bool { stored }
35+
}
36+
func applySettingsMigration(context: Context) {
37+
let archiveSettings = context.engine.settings
38+
'''
39+
tests = '''
40+
}
41+
for alreadyConfigured in [false, true] {
42+
let engine = Engine(Settings(legacyLockPasswordHash: "legacy", isPasswordConfigured: alreadyConfigured))
43+
ArchivePasswordKeychain.stored = false
44+
applySettingsMigration(context: Context(engine: engine))
45+
precondition(engine.settings.legacyLockPasswordHash == "legacy", "Lost only credential after failed migration")
46+
precondition(engine.settings.isPasswordConfigured == alreadyConfigured)
47+
ArchivePasswordKeychain.stored = true
48+
applySettingsMigration(context: Context(engine: engine))
49+
precondition(engine.settings.legacyLockPasswordHash == nil)
50+
precondition(engine.settings.isPasswordConfigured, "Notification protection was not mirrored")
51+
}
52+
print("Archive migration preserves credentials on failure and mirrors protection on success: passed")
53+
'''
54+
with tempfile.TemporaryDirectory(prefix='archive-migration-') as tmp:
55+
path = pathlib.Path(tmp) / 'main.swift'
56+
path.write_text(fixtures + source[start:end] + tests, encoding='utf-8')
57+
subprocess.run(['swift', str(path)], check=True)

0 commit comments

Comments
 (0)