From 7ba939a8e6063149617c646fcb14a6b939ca53ca Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 16 Sep 2026 20:10:45 +0300 Subject: [PATCH 1/2] fix: refresh paykit key after wallet reset --- Bitkit/Services/PubkyService.swift | 6 ++- .../PaykitReceiverNoiseKeyStoreTests.swift | 43 +++++++++++++++++++ changelog.d/next/761.fixed.md | 1 + 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 changelog.d/next/761.fixed.md diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift index c1117fe38..ba587f9ff 100644 --- a/Bitkit/Services/PubkyService.swift +++ b/Bitkit/Services/PubkyService.swift @@ -1490,16 +1490,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. From 8b98438c9e6a4361b6659084430ebe3c3ca6db13 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 24 Sep 2026 07:47:13 +0100 Subject: [PATCH 2/2] test: cover cached keychain read failure --- .../PaykitReceiverNoiseKeyStoreTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift b/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift index de0638def..04327f9ca 100644 --- a/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift +++ b/BitkitTests/PaykitReceiverNoiseKeyStoreTests.swift @@ -97,6 +97,34 @@ final class PaykitReceiverNoiseKeyStoreTests: XCTestCase { } } + func testCachedKeyDoesNotBypassKeychainReadFailure() throws { + enum LoadError: Error { + case inaccessible + } + + let derivedBytes = Data(repeating: 1, count: 32) + var shouldFailLoad = false + var upsertCount = 0 + let store = PaykitReceiverNoiseKeyStore( + loadBytes: { + if shouldFailLoad { + throw LoadError.inaccessible + } + return derivedBytes + }, + upsertBytes: { _ in upsertCount += 1 }, + deriveBytes: { derivedBytes } + ) + _ = try store.loadOrDerive() + + shouldFailLoad = true + + XCTAssertThrowsError(try store.loadOrDerive()) { error in + XCTAssertTrue(error is LoadError) + } + XCTAssertEqual(upsertCount, 0) + } + func testRejectsInvalidPersistedReceiverNoiseKey() { let store = PaykitReceiverNoiseKeyStore( loadBytes: { Data(repeating: 0, count: 31) },