iOS: Debug Serial Number (#69017)

* Move SQLite files into a SQLite subfolder

* Add a debug-only user defaults store

* Add a client for retrieving the device serial number

In debug builds, the serial number is not available (because it's only available through MDM managed configs) so we fake it by reaching into the debug user defaults and storing something there.

* Add a debug settings view

This is where we can noodle/iterate on settings that only affect debug build behaviors.

* Resolve some Xcode 27 warnings

* Remove code signing from intermediate build products, deferring them to the final build

* Update FakeSerialNumberGenerator doc comment (and change it to an enum)
This commit is contained in:
Gustavo Medori
2026-08-03 18:19:32 +00:00
committed by GitHub
parent e930cf7591
commit c4ec12dd59
19 changed files with 273 additions and 13 deletions
-2
View File
@@ -34,7 +34,6 @@ public enum EnrollClientError: Error, Sendable {
case clientCreationFailed
}
extension EnrollClient {
public static let liveValue = EnrollClient(
requestEnrollmentToken: { hostName, port, pairingToken in
@@ -53,4 +52,3 @@ extension EnrollClient {
},
)
}
@@ -266,7 +266,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2650;
LastUpgradeCheck = 2650;
LastUpgradeCheck = 2700;
TargetAttributes = {
0C94C8512FA232E600C13C9A = {
CreatedOnToolsVersion = 26.4.1;
@@ -651,6 +651,7 @@
isa = XCBuildConfiguration;
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K497G57PDJ;
@@ -706,6 +707,7 @@
isa = XCBuildConfiguration;
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K497G57PDJ;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2650"
LastUpgradeVersion = "2700"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2650"
LastUpgradeVersion = "2700"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2650"
LastUpgradeVersion = "2700"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -24,4 +24,7 @@ extension DependencyValues {
@DependencyEntry(liveValue: DeviceTrustCredentialClient.liveValue)
nonisolated var deviceTrustCredentialClient = DeviceTrustCredentialClient()
@DependencyEntry(liveValue: SerialNumberClient.liveValue)
nonisolated var serialNumberClient = SerialNumberClient()
}
@@ -0,0 +1,33 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
#if DEBUG
import Foundation
/// A small utility for generating a fake serial number, mostly to support the debug-only serial number setting.
enum FakeSerialNumberGenerator {
private static let base: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
static func generate() -> String {
func chunk() -> String {
(0 ..< 4)
.compactMap { _ in Self.base.randomElement() }
.reduce(into: "") { $0.append($1) }
}
return "\(chunk())-\(chunk())"
}
}
#endif
@@ -0,0 +1,44 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
import DependenciesMacros
import Foundation
import Sharing
@DependencyClient
struct SerialNumberClient {
var getDeviceSerialNumber: @Sendable () -> String = { "" }
}
extension SerialNumberClient {
static let liveValue = SerialNumberClient(
getDeviceSerialNumber: {
#if DEBUG
@Shared(.debugStorage(.debugSerialNumber))
var debugSerialNumber: String? = nil
guard let debugSerialNumber else {
let newDebugSerialNumber = FakeSerialNumberGenerator.generate()
$debugSerialNumber.withLock { $0 = newDebugSerialNumber }
return newDebugSerialNumber
}
return debugSerialNumber
#else
fatalError("Production path has not yet been implemented. Requires messing around in Jamf.")
#endif
},
)
}
@@ -0,0 +1,43 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
#if DEBUG
import SwiftUI
struct DebugSettingsView: View {
@Bindable
var viewModel: DebugSettingsViewModel
var body: some View {
Form {
Section("Serial Number") {
HStack {
TextField("Serial Number", text: viewModel.debugSerialNumberBinding)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: .infinity, alignment: .leading)
Button {
viewModel.regenerateSerialNumber()
} label: {
Image(systemName: "arrow.circlepath")
}
}
}
}
}
}
#endif
@@ -0,0 +1,47 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
#if DEBUG
import Dependencies
import Foundation
import Sharing
import SwiftUI
@Observable @MainActor
final class DebugSettingsViewModel {
@ObservationIgnored
@Dependency(\.serialNumberClient)
var serialNumberClient
@ObservationIgnored
@Shared(.debugStorage(.debugSerialNumber))
var debugSerialNumber: String? = nil
}
// MARK: - Serial Number
extension DebugSettingsViewModel {
var debugSerialNumberBinding: Binding<String> {
Binding($debugSerialNumber.emptyIfNil)
}
func regenerateSerialNumber() {
$debugSerialNumber.withLock { $0 = FakeSerialNumberGenerator.generate() }
}
}
#endif
@@ -30,11 +30,14 @@
var body: some View {
NavigationStack {
List {
Group {
Section("Feature Demos") {
Button("Secure Enclave Storage", systemImage: "lock") {
viewModel.destination = .deviceTrustCredentialDemo(.init())
}
Section("Settings") {
Button("Debug Settings", systemImage: "gearshape") {
viewModel.destination = .debugSettingsView(.init())
}
}
Section("Feature Demos") {
Button("Secure Enclave Storage", systemImage: "lock") {
viewModel.destination = .deviceTrustCredentialDemo(.init())
}
}
}
@@ -45,6 +48,9 @@
.navigationDestination(item: $viewModel.destination.deviceTrustCredentialDemo) { viewModel in
FeatureDemo.DeviceTrustCredentialView(viewModel: viewModel)
}
.navigationDestination(item: $viewModel.destination.debugSettingsView) { viewModel in
DebugSettingsView(viewModel: viewModel)
}
}
}
}
@@ -23,6 +23,10 @@
final class DebugViewModel {
@CasePathable
enum Destination {
// MARK: - Debug Settings
case debugSettingsView(DebugSettingsViewModel)
// MARK: - Feature Demos
case deviceTrustCredentialDemo(FeatureDemo.DeviceTrustCredentialViewModel)
@@ -47,7 +47,7 @@ struct LandingView: View {
.background(Color.Background.depth3)
// MARK: Toolbar
.toolbarVisibility(viewModel.shouldShowToolbar ? .visible : .hidden)
.toolbar {
ToolbarItem {
@@ -101,7 +101,6 @@ struct LandingView: View {
}
#endif
// swiftformat:enable indent
}
}
}
@@ -0,0 +1,52 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
#if DEBUG
import Foundation
import Sharing
extension UserDefaults {
static let debugSuiteName = "com.gravitational.teleport.verify.debug"
/// A UserDefaults suite that's only available in debug builds.
///
/// This is marked as `nonisolated(unsafe)` because UserDefaults isn't marked Sendable, but it _is_ thread safe.
/// See <https://developer.apple.com/documentation/foundation/userdefaults>
nonisolated(unsafe) static let debug: UserDefaults = {
guard let store = UserDefaults(suiteName: debugSuiteName) else {
preconditionFailure("Could not instantiate debug user defaults.")
}
return store
}()
}
// MARK: - Keys
/// Type-safe keys for use when querying the debug user defaults
enum DebugUserDefaultsKey: String {
case debugSerialNumber
}
/// A small helper function to access the debug store via the Sharing library
extension SharedReaderKey where Self == AppStorageKey<String?> {
static func debugStorage(
_ key: DebugUserDefaultsKey,
) -> Self {
.appStorage(key.rawValue, store: .debug)
}
}
#endif
@@ -0,0 +1,29 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
import Foundation
extension String? {
var emptyIfNil: String {
get {
guard let self else { return "" }
return self
}
set {
self = newValue
}
}
}