summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadSettings/SettingsManager.swift
blob: 852c8e0856a3ee0e90f3fd584d74d5cb779f83d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
//
//  SettingsManager.swift
//  MullvadVPN
//
//  Created by pronebird on 29/04/2022.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadLogging
import MullvadTypes

private let keychainServiceName = "Mullvad VPN"
private let accountTokenKey = "accountToken"
private let accountExpiryKey = "accountExpiry"

public enum SettingsManager {
    nonisolated(unsafe) private static let logger = Logger(label: "SettingsManager")

    #if DEBUG
        nonisolated(unsafe) private static var _store = KeychainSettingsStore(
            serviceName: keychainServiceName,
            accessGroup: ApplicationConfiguration.securityGroupIdentifier
        )

        /// Alternative store used for tests.
        nonisolated(unsafe) internal static var unitTestStore: SettingsStore?

        public static var store: SettingsStore {
            if let unitTestStore { return unitTestStore }
            return _store
        }

    #else
        public static let store: SettingsStore = KeychainSettingsStore(
            serviceName: keychainServiceName,
            accessGroup: ApplicationConfiguration.securityGroupIdentifier
        )

    #endif

    private static func makeParser() -> SettingsParser {
        SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
    }

    // MARK: - Last used account

    public static func getLastUsedAccount() throws -> String {
        let data = try store.read(key: .lastUsedAccount)
        guard let result = String(bytes: data, encoding: .utf8) else {
            throw StringDecodingError(data: data)
        }
        return result
    }

    public static func setLastUsedAccount(_ string: String?) throws {
        if let string {
            guard let data = string.data(using: .utf8) else {
                throw StringEncodingError(string: string)
            }

            try store.write(data, for: .lastUsedAccount)
        } else {
            do {
                try store.delete(key: .lastUsedAccount)
            } catch let error as KeychainError where error == .itemNotFound {
                return
            } catch {
                throw error
            }
        }
    }

    // MARK: - Should wipe settings

    public static func getShouldWipeSettings() -> Bool {
        (try? store.read(key: .shouldWipeSettings)) != nil
    }

    public static func setShouldWipeSettings() {
        do {
            try store.write(Data(), for: .shouldWipeSettings)
        } catch {
            logger.error(
                error: error,
                message: "Failed to set should wipe settings."
            )
        }
    }

    // MARK: - Settings

    public static func readSettings() throws -> LatestTunnelSettings {
        let storedVersion: Int
        let data: Data
        let parser = makeParser()

        do {
            data = try store.read(key: .settings)
            storedVersion = try parser.parseVersion(data: data)
        } catch {
            throw ReadSettingsVersionError(underlyingError: error)
        }

        let currentVersion = SchemaVersion.current

        if storedVersion == currentVersion.rawValue {
            return try parser.parsePayload(as: LatestTunnelSettings.self, from: data)
        } else {
            throw UnsupportedSettingsVersionError(
                storedVersion: storedVersion,
                currentVersion: currentVersion
            )
        }
    }

    public static func writeSettings(_ settings: LatestTunnelSettings) throws {
        let parser = makeParser()
        let data = try parser.producePayload(settings, version: SchemaVersion.current.rawValue)

        try store.write(data, for: .settings)
    }

    // MARK: - Device state

    public static func readDeviceState() throws -> DeviceState {
        let data = try store.read(key: .deviceState)
        let parser = makeParser()

        return try parser.parseUnversionedPayload(as: DeviceState.self, from: data)
    }

    public static func writeDeviceState(_ deviceState: DeviceState) throws {
        let parser = makeParser()
        let data = try parser.produceUnversionedPayload(deviceState)

        try store.write(data, for: .deviceState)
    }

    /// Removes all legacy settings, device state, tunnel settings and API access methods but keeps
    /// the last used account number stored.
    public static func resetStore(completely: Bool = false) {
        logger.debug("Reset store.")

        let keys =
            completely
            ? SettingsKey.allCases
            : [
                .settings,
                .deviceState,
                .apiAccessMethods,
                .ipOverrides,
                .customRelayLists,
            ]

        keys.forEach { key in
            do {
                try store.delete(key: key)
            } catch {
                if (error as? KeychainError) != .itemNotFound {
                    logger.error(error: error, message: "Failed to delete \(key.rawValue).")
                }
            }
        }
    }

    // MARK: - Private

    private static func checkLatestSettingsVersion() throws {
        let settingsVersion: Int
        do {
            let parser = makeParser()
            let settingsData = try store.read(key: .settings)
            settingsVersion = try parser.parseVersion(data: settingsData)
        } catch .itemNotFound as KeychainError {
            return
        } catch {
            throw ReadSettingsVersionError(underlyingError: error)
        }

        guard settingsVersion != SchemaVersion.current.rawValue else {
            return
        }

        let error = UnsupportedSettingsVersionError(
            storedVersion: settingsVersion,
            currentVersion: SchemaVersion.current
        )

        logger.error(error: error, message: "Encountered an unknown version.")

        throw error
    }
}

// MARK: - Supporting types

/// An error type describing a failure to read or parse settings version.
public struct ReadSettingsVersionError: LocalizedError, WrappingError {
    private let inner: Error

    public var underlyingError: Error? {
        inner
    }

    public var errorDescription: String? {
        "Failed to read settings version."
    }

    public init(underlyingError: Error) {
        inner = underlyingError
    }
}

/// An error returned when stored settings version is unknown to the currently running app.
public struct UnsupportedSettingsVersionError: LocalizedError {
    public let storedVersion: Int
    public let currentVersion: SchemaVersion

    public var errorDescription: String? {
        """
        Stored settings version was not the same as current version, \
        stored version: \(storedVersion), current version: \(currentVersion)
        """
    }
}