diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift index a08c8410c..6c56b0c8a 100644 --- a/Bitkit/Services/PubkyService.swift +++ b/Bitkit/Services/PubkyService.swift @@ -1565,16 +1565,18 @@ final class PaykitReceiverNoiseKeyStore: @unchecked Sendable { } private func validatedKeyBytes() throws -> Data { - if let validatedBytes { + let storedBytes = try loadBytes() + if let validatedBytes, storedBytes == validatedBytes { return validatedBytes } + validatedBytes = nil let derivedBytes = try deriveBytes() guard derivedBytes.count == Self.keyLength else { throw invalidKeyError("Derived Paykit receiver Noise key is invalid") } - if let storedBytes = try loadBytes() { + if let storedBytes { guard storedBytes.count == Self.keyLength else { throw invalidKeyError("Stored Paykit receiver Noise key is invalid") } diff --git a/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift b/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift index 017b2828d..de0638def 100644 --- a/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift +++ b/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift @@ -54,6 +54,49 @@ final class PaykitReceiverNoiseKeyStoreTests: XCTestCase { XCTAssertEqual(persistedBytes, Data(repeating: 1, count: 32)) } + func testReceiverNoiseKeyFollowsWalletReplacementAfterKeychainWipe() throws { + var persistedBytes: Data? + var derivedBytes = Data(repeating: 1, count: 32) + let store = PaykitReceiverNoiseKeyStore( + loadBytes: { persistedBytes }, + upsertBytes: { persistedBytes = $0 }, + deriveBytes: { derivedBytes } + ) + let previousKey = try store.loadOrDerive() + + persistedBytes = nil + derivedBytes = Data(repeating: 2, count: 32) + + let replacementKey = try store.loadOrDerive().exportBytes() + XCTAssertEqual(replacementKey, derivedBytes) + XCTAssertEqual(persistedBytes, derivedBytes) + XCTAssertThrowsError(try store.persist(previousKey)) + + let restoredStore = PaykitReceiverNoiseKeyStore( + loadBytes: { persistedBytes }, + upsertBytes: { persistedBytes = $0 }, + deriveBytes: { derivedBytes } + ) + XCTAssertEqual(try restoredStore.loadOrDerive().exportBytes(), replacementKey) + } + + func testRejectsChangedPersistedKeyAfterCaching() throws { + let derivedBytes = Data(repeating: 1, count: 32) + var persistedBytes: Data? = derivedBytes + let store = PaykitReceiverNoiseKeyStore( + loadBytes: { persistedBytes }, + upsertBytes: { _ in XCTFail("Invalid bytes must not be overwritten") }, + deriveBytes: { derivedBytes } + ) + _ = try store.loadOrDerive() + + for invalidBytes in [Data(repeating: 1, count: 31), Data(repeating: 2, count: 32)] { + persistedBytes = invalidBytes + XCTAssertThrowsError(try store.loadOrDerive()) + XCTAssertEqual(persistedBytes, invalidBytes) + } + } + func testRejectsInvalidPersistedReceiverNoiseKey() { let store = PaykitReceiverNoiseKeyStore( loadBytes: { Data(repeating: 0, count: 31) }, diff --git a/changelog.d/next/761.fixed.md b/changelog.d/next/761.fixed.md new file mode 100644 index 000000000..d5aade432 --- /dev/null +++ b/changelog.d/next/761.fixed.md @@ -0,0 +1 @@ +Fixed private payment connections reusing a previous wallet’s key after resetting the wallet.