Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions Bitkit/Services/LightningService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class LightningService {
let storeId = try await VssStoreIdProvider.shared.getVssStoreId(walletIndex: walletIndex)

let vssUrl = Env.vssServerUrl
let lnurlAuthServerUrl = Env.lnurlAuthServerUrl
let lnurlAuthServerUrl = Env.lnurlAuthServerUrl.trimmingCharacters(in: .whitespacesAndNewlines)
Comment thread
jvsena42 marked this conversation as resolved.
Logger.debug("Building ldk-node with vssUrl: '\(vssUrl)'")
Logger.debug("Building ldk-node with lnurlAuthServerUrl: '\(lnurlAuthServerUrl)'")

Expand All @@ -160,21 +160,17 @@ class LightningService {

builder.setEntropyBip39Mnemonic(mnemonic: mnemonic, passphrase: passphrase)

guard !lnurlAuthServerUrl.isEmpty else {
throw CustomServiceError.vssAuthRequired
}

try await ServiceQueue.background(.ldk) {
if !lnurlAuthServerUrl.isEmpty {
self.node = try builder.buildWithVssStore(
vssUrl: vssUrl,
storeId: storeId,
lnurlAuthServerUrl: lnurlAuthServerUrl,
fixedHeaders: [:]
)
} else {
self.node = try builder.buildWithVssStoreAndFixedHeaders(
vssUrl: vssUrl,
storeId: storeId,
fixedHeaders: [:]
)
}
self.node = try builder.buildWithVssStore(
vssUrl: vssUrl,
storeId: storeId,
lnurlAuthServerUrl: lnurlAuthServerUrl,
fixedHeaders: [:]
)
}
shouldReleaseLightningLock = false

Expand Down
13 changes: 12 additions & 1 deletion Bitkit/Services/MigrationsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ enum RNKeychainKey {

// MARK: - Channel Migration Data

struct PendingChannelMigration: Codable {
struct PendingChannelMigration: Codable, Equatable {
let channelManager: Data
let channelMonitors: [Data]
}
Expand Down Expand Up @@ -401,6 +401,17 @@ class MigrationsService: ObservableObject {
set { setCodable(newValue, forKey: Self.rnPendingChannelMigrationKey) }
}

func withPendingChannelMigration(
_ operation: (PendingChannelMigration?) async throws -> Void
) async rethrows {
let migration = pendingChannelMigration
try await operation(migration)

if pendingChannelMigration == migration {
pendingChannelMigration = nil
}
}

/// Stored activity data from RN remote backup for reapplying metadata after sync (persisted)
var pendingRemoteActivityData: [RNActivityItem]? {
get { getCodable(forKey: Self.rnPendingRemoteActivityDataKey) }
Expand Down
26 changes: 12 additions & 14 deletions Bitkit/Services/VssBackupClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ class VssBackupClient {
await ldkSetupCoordinator.reset()
}

/// Returns lnurl auth params when lnurl is configured; nil otherwise.
/// Returns LNURL-auth params when configured. Callers fail closed if this returns nil.
private func getLnurlAuthParams(walletIndex: Int) async throws
-> (vssUrl: String, storeId: String, mnemonic: String, passphrase: String?, lnurlAuthServerUrl: String)?
{
let lnurlAuthServerUrl = Env.lnurlAuthServerUrl
let lnurlAuthServerUrl = Env.lnurlAuthServerUrl.trimmingCharacters(in: .whitespacesAndNewlines)
guard !lnurlAuthServerUrl.isEmpty else { return nil }
guard let mnemonic = try Keychain.loadString(key: .bip39Mnemonic(index: walletIndex)) else {
throw CustomServiceError.mnemonicNotFound
Expand All @@ -87,18 +87,16 @@ class VssBackupClient {
let vssUrl = Env.vssServerUrl
Logger.debug("Building VSS client with vssUrl: '\(vssUrl)'", context: "VssBackupClient")

if let params = try await getLnurlAuthParams(walletIndex: walletIndex) {
try await vssNewClientWithLnurlAuth(
baseUrl: params.vssUrl,
storeId: params.storeId,
mnemonic: params.mnemonic,
passphrase: params.passphrase,
lnurlAuthServerUrl: params.lnurlAuthServerUrl
)
} else {
let storeId = try await VssStoreIdProvider.shared.getVssStoreId(walletIndex: walletIndex)
try await vssNewClient(baseUrl: vssUrl, storeId: storeId)
guard let params = try await getLnurlAuthParams(walletIndex: walletIndex) else {
throw CustomServiceError.vssAuthRequired
}
try await vssNewClientWithLnurlAuth(
baseUrl: params.vssUrl,
storeId: params.storeId,
mnemonic: params.mnemonic,
passphrase: params.passphrase,
lnurlAuthServerUrl: params.lnurlAuthServerUrl
)
Logger.info("VSS client setup with server: '\(vssUrl)'", context: "VssBackupClient")
}
} catch {
Expand All @@ -110,7 +108,7 @@ class VssBackupClient {
/// Lazily initializes the LDK VSS client (used only by the debug screen). Only runs when lnurl auth is configured.
private func setupLdk(walletIndex: Int = 0) async throws {
guard let params = try await getLnurlAuthParams(walletIndex: walletIndex) else {
throw AppError(message: "LDK VSS requires lnurl auth", debugMessage: "lnurlAuthServerUrl is not set")
throw CustomServiceError.vssAuthRequired
}
do {
try await withTimeout(seconds: 30) {
Expand Down
6 changes: 6 additions & 0 deletions Bitkit/Utilities/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ enum CustomServiceError: LocalizedError {
case nodeNotStarted
case onchainWalletNotInitialized
case mnemonicNotFound
case vssAuthRequired
case nodeStillRunning
case onchainWalletStillRunning
case invalidNodeSigningMessage
Expand All @@ -23,6 +24,8 @@ enum CustomServiceError: LocalizedError {
return "Onchain wallet not created"
case .mnemonicNotFound:
return "Mnemonic not found"
case .vssAuthRequired:
return "VSS requires LNURL-auth"
case .nodeStillRunning:
return "Node is still running"
case .onchainWalletStillRunning:
Expand Down Expand Up @@ -144,6 +147,9 @@ struct AppError: LocalizedError {
case .mnemonicNotFound:
message = "Mnemonic not found"
debugMessage = nil
case .vssAuthRequired:
message = "VSS requires LNURL-auth"
debugMessage = nil
case .nodeStillRunning:
message = "Node is still running"
debugMessage = nil
Expand Down
30 changes: 15 additions & 15 deletions Bitkit/ViewModels/WalletViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,23 @@ class WalletViewModel: ObservableObject {
let electrumServerUrl = electrumConfigService.getCurrentServer().fullUrl
let rgsServerUrl = rgsConfigService.getCurrentServerUrl()

var channelMigration: ChannelDataMigration?
if let migration = MigrationsService.shared.pendingChannelMigration {
channelMigration = ChannelDataMigration(
channelManager: [UInt8](migration.channelManager),
channelMonitors: migration.channelMonitors.map { [UInt8]($0) }
)
MigrationsService.shared.pendingChannelMigration = nil
}
try await MigrationsService.shared.withPendingChannelMigration { migration in
let channelMigration = migration.map {
ChannelDataMigration(
channelManager: [UInt8]($0.channelManager),
channelMonitors: $0.channelMonitors.map { [UInt8]($0) }
)
}

await runLegacyNetworkGraphCleanupIfNeeded()
await runLegacyNetworkGraphCleanupIfNeeded()

try await lightningService.setup(
walletIndex: walletIndex,
electrumServerUrl: electrumServerUrl,
rgsServerUrl: rgsServerUrl.isEmpty ? nil : rgsServerUrl,
channelMigration: channelMigration
)
try await lightningService.setup(
walletIndex: walletIndex,
electrumServerUrl: electrumServerUrl,
rgsServerUrl: rgsServerUrl.isEmpty ? nil : rgsServerUrl,
channelMigration: channelMigration
)
}
try await lightningService.start(onEvent: { event in
Task { @MainActor in
// Notify all event handlers
Expand Down
69 changes: 69 additions & 0 deletions BitkitTests/ChannelMigrationPersistenceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@testable import Bitkit
import XCTest

final class ChannelMigrationPersistenceTests: XCTestCase {
private enum SetupError: Error {
case failed
}

private let migrations = MigrationsService.shared

override func setUp() {
super.setUp()
migrations.pendingChannelMigration = nil
}

override func tearDown() {
migrations.pendingChannelMigration = nil
super.tearDown()
}

func testPendingMigrationIsRetainedWhenSetupFails() async {
let migration = makeMigration(seed: 1)
migrations.pendingChannelMigration = migration

do {
try await migrations.withPendingChannelMigration { pendingMigration in
XCTAssertEqual(pendingMigration, migration)
throw SetupError.failed
}
XCTFail("Expected setup to fail")
} catch SetupError.failed {
} catch {
XCTFail("Unexpected error: \(error)")
}

XCTAssertEqual(migrations.pendingChannelMigration, migration)
}

func testPendingMigrationIsClearedAfterSetupSucceeds() async {
let migration = makeMigration(seed: 2)
migrations.pendingChannelMigration = migration

await migrations.withPendingChannelMigration { pendingMigration in
XCTAssertEqual(pendingMigration, migration)
}

XCTAssertNil(migrations.pendingChannelMigration)
}

func testNewPendingMigrationIsNotClearedAfterSetupSucceeds() async {
let migration = makeMigration(seed: 3)
let replacement = makeMigration(seed: 4)
migrations.pendingChannelMigration = migration

await migrations.withPendingChannelMigration { pendingMigration in
XCTAssertEqual(pendingMigration, migration)
migrations.pendingChannelMigration = replacement
}

XCTAssertEqual(migrations.pendingChannelMigration, replacement)
}

private func makeMigration(seed: UInt8) -> PendingChannelMigration {
PendingChannelMigration(
channelManager: Data([seed]),
channelMonitors: [Data([seed, seed &+ 1])]
)
}
}
1 change: 1 addition & 0 deletions changelog.d/next/665.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Wallet backups no longer fall back to unauthenticated VSS when LNURL-auth is missing.
Loading