summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-01-03 11:43:10 +0100
committerAndrej Mihajlov <and@mullvad.net>2020-01-03 11:43:10 +0100
commitdaee16640792a5a99de3954dbc80bf775f023497 (patch)
tree69a05f974d2a0e261b91f6a50c90f6ff4e706936
parentf424194d3a57c1f6c4dcb349ee8b93c138f3c1f3 (diff)
parent9f31769e8801b25619f33fe7ca64ccd824a15f0b (diff)
downloadmullvadvpn-daee16640792a5a99de3954dbc80bf775f023497.tar.xz
mullvadvpn-daee16640792a5a99de3954dbc80bf775f023497.zip
Merge branch 'regenerate-private-key-ios'
-rw-r--r--ios/MullvadVPN/JsonRpc.swift17
-rw-r--r--ios/MullvadVPN/MullvadAPI.swift108
-rw-r--r--ios/MullvadVPN/MutuallyExclusive.swift108
-rw-r--r--ios/MullvadVPN/RelayCache.swift4
-rw-r--r--ios/MullvadVPN/TunnelManager.swift106
-rw-r--r--ios/MullvadVPN/WireguardPrivateKey.swift59
6 files changed, 328 insertions, 74 deletions
diff --git a/ios/MullvadVPN/JsonRpc.swift b/ios/MullvadVPN/JsonRpc.swift
index 6c54bf6adb..58a6b01827 100644
--- a/ios/MullvadVPN/JsonRpc.swift
+++ b/ios/MullvadVPN/JsonRpc.swift
@@ -38,8 +38,10 @@ struct JsonRpcRequest: Encodable {
}
}
-class JsonRpcResponseError: Error, Decodable {
- let code: Int
+class JsonRpcResponseError<ResponseCode>: Error, Decodable
+ where ResponseCode: Decodable
+{
+ let code: ResponseCode
let message: String
var localizedDescription: String? {
@@ -53,15 +55,18 @@ class JsonRpcResponseError: Error, Decodable {
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
- code = try container.decode(Int.self, forKey: .code)
+ code = try container.decode(ResponseCode.self, forKey: .code)
message = try container.decode(String.self, forKey: .message)
}
}
-struct JsonRpcResponse<T: Decodable>: Decodable {
+struct JsonRpcResponse<T, ResponseCode>: Decodable
+ where
+ T: Decodable, ResponseCode: Decodable
+{
let version: String
let id: String
- let result: Result<T, JsonRpcResponseError>
+ let result: Result<T, JsonRpcResponseError<ResponseCode>>
private enum CodingKeys: String, CodingKey {
case version = "jsonrpc", id, result, error
@@ -76,7 +81,7 @@ struct JsonRpcResponse<T: Decodable>: Decodable {
if container.contains(.result) {
self.result = .success(try container.decode(T.self, forKey: .result))
} else {
- self.result = .failure(try container.decode(JsonRpcResponseError.self, forKey: .error))
+ self.result = .failure(try container.decode(JsonRpcResponseError<ResponseCode>.self, forKey: .error))
}
}
}
diff --git a/ios/MullvadVPN/MullvadAPI.swift b/ios/MullvadVPN/MullvadAPI.swift
index ebe8956830..be87728b7f 100644
--- a/ios/MullvadVPN/MullvadAPI.swift
+++ b/ios/MullvadVPN/MullvadAPI.swift
@@ -16,18 +16,6 @@ private let kMullvadAPIURL = URL(string: "https://api.mullvad.net/rpc/")!
/// Network request timeout in seconds
private let kNetworkTimeout: TimeInterval = 10
-/// An error type emitted by `MullvadAPI`
-enum MullvadAPIError: Error {
- /// A network communication error
- case network(URLError)
-
- /// An error occured when decoding the JSON response
- case decoding(Error)
-
- /// An error occured when encoding the JSON request
- case encoding(Error)
-}
-
/// A type that describes the account verification result
enum AccountVerification {
/// The app should attempt to verify the account token at some point later because the network
@@ -44,29 +32,79 @@ enum AccountVerification {
/// An error type that describes why the account verification was deferred
enum DeferReasonError: Error {
/// Mullvad API communication error
- case communication(MullvadAPIError)
+ case communication(MullvadAPI.Error)
/// Mullvad API responded with an error
- case server(JsonRpcResponseError)
+ case server(MullvadAPI.ResponseError)
}
-/// The error code returned by the API when it cannot find the given account token
-private let kAccountDoesNotExistErrorCode = -200
-
class MullvadAPI {
private let session: URLSession
+ /// A enum mapping the integer codes returned by Mullvad API with the corresponding enum
+ /// variants
+ private enum RawResponseCode: Int {
+ case accountDoesNotExist = -200
+ case tooManyWireguardKeys = -703
+ }
+
+ /// A enum describing the Mullvad API response code
+ enum ResponseCode: RawRepresentable, Codable {
+ var rawValue: Int {
+ switch self {
+ case .accountDoesNotExist:
+ return RawResponseCode.accountDoesNotExist.rawValue
+
+ case .tooManyWireguardKeys:
+ return RawResponseCode.tooManyWireguardKeys.rawValue
+
+ case .other(let value):
+ return value
+ }
+ }
+
+ init?(rawValue: Int) {
+ switch RawResponseCode(rawValue: rawValue) {
+ case .accountDoesNotExist:
+ self = .accountDoesNotExist
+ case .tooManyWireguardKeys:
+ self = .tooManyWireguardKeys
+ case .none:
+ self = ResponseCode.other(rawValue)
+ }
+ }
+
+ case accountDoesNotExist
+ case tooManyWireguardKeys
+ case other(Int)
+ }
+
+ /// An error type emitted by `MullvadAPI`
+ enum Error: Swift.Error {
+ /// A network communication error
+ case network(URLError)
+
+ /// An error occured when decoding the JSON response
+ case decoding(Swift.Error)
+
+ /// An error occured when encoding the JSON request
+ case encoding(Swift.Error)
+ }
+
+ typealias ResponseError = JsonRpcResponseError<ResponseCode>
+ typealias Response<T: Decodable> = JsonRpcResponse<T, ResponseCode>
+
init(session: URLSession = URLSession.shared) {
self.session = session
}
- func getRelayList() -> AnyPublisher<JsonRpcResponse<RelayList>, MullvadAPIError> {
+ func getRelayList() -> AnyPublisher<Response<RelayList>, MullvadAPI.Error> {
let request = JsonRpcRequest(method: "relay_list_v3", params: [])
return MullvadAPI.makeDataTaskPublisher(request: request)
}
- func getAccountExpiry(accountToken: String) -> AnyPublisher<JsonRpcResponse<Date>, MullvadAPIError> {
+ func getAccountExpiry(accountToken: String) -> AnyPublisher<Response<Date>, MullvadAPI.Error> {
let request = JsonRpcRequest(method: "get_expiry", params: [AnyEncodable(accountToken)])
return MullvadAPI.makeDataTaskPublisher(request: request)
@@ -81,7 +119,7 @@ class MullvadAPI {
return .verified(expiry)
case .failure(let serverError):
- if serverError.code == kAccountDoesNotExistErrorCode {
+ if case .accountDoesNotExist = serverError.code {
// Report .invalid account if the server responds with the special code
return .invalid
} else {
@@ -91,13 +129,13 @@ class MullvadAPI {
}
})
.catch({ (networkError) in
- // Treat all network errors as .deferred verification
+ // Treat all communication errors as .deferred verification
return Just(.deferred(.communication(networkError)))
})
.eraseToAnyPublisher()
}
- func pushWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<JsonRpcResponse<WireguardAssociatedAddresses>, MullvadAPIError> {
+ func pushWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<Response<WireguardAssociatedAddresses>, MullvadAPI.Error> {
let request = JsonRpcRequest(method: "push_wg_key", params: [
AnyEncodable(accountToken),
AnyEncodable(publicKey)
@@ -106,7 +144,17 @@ class MullvadAPI {
return MullvadAPI.makeDataTaskPublisher(request: request)
}
- func checkWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<JsonRpcResponse<WireguardAssociatedAddresses>, MullvadAPIError> {
+ func replaceWireguardKey(accountToken: String, oldPublicKey: Data, newPublicKey: Data) -> AnyPublisher<Response<WireguardAssociatedAddresses>, MullvadAPI.Error> {
+ let request = JsonRpcRequest(method: "replace_wg_key", params: [
+ AnyEncodable(accountToken),
+ AnyEncodable(oldPublicKey),
+ AnyEncodable(newPublicKey)
+ ])
+
+ return MullvadAPI.makeDataTaskPublisher(request: request)
+ }
+
+ func checkWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<Response<Bool>, MullvadAPI.Error> {
let request = JsonRpcRequest(method: "check_wg_key", params: [
AnyEncodable(accountToken),
AnyEncodable(publicKey)
@@ -115,17 +163,19 @@ class MullvadAPI {
return MullvadAPI.makeDataTaskPublisher(request: request)
}
- private static func makeDataTaskPublisher<T: Decodable>(request: JsonRpcRequest) -> AnyPublisher<JsonRpcResponse<T>, MullvadAPIError> {
+ private static func makeDataTaskPublisher<T: Decodable>(request: JsonRpcRequest) -> AnyPublisher<Response<T>, MullvadAPI.Error> {
return Just(request)
.encode(encoder: makeJSONEncoder())
- .mapError { MullvadAPIError.encoding($0) }
+ .mapError { MullvadAPI.Error.encoding($0) }
.map { self.makeURLRequest(httpBody: $0) }
.flatMap {
URLSession.shared.dataTaskPublisher(for: $0)
- .mapError { MullvadAPIError.network($0) }
- .map { $0.data }
- .decode(type: JsonRpcResponse<T>.self, decoder: makeJSONDecoder())
- .mapError { MullvadAPIError.decoding($0) }
+ .mapError { MullvadAPI.Error.network($0) }
+ .flatMap { (data, response) in
+ Just(data)
+ .decode(type: Response<T>.self, decoder: makeJSONDecoder())
+ .mapError { MullvadAPI.Error.decoding($0) }
+ }
}.eraseToAnyPublisher()
}
diff --git a/ios/MullvadVPN/MutuallyExclusive.swift b/ios/MullvadVPN/MutuallyExclusive.swift
index cb3803805a..c93cc5362e 100644
--- a/ios/MullvadVPN/MutuallyExclusive.swift
+++ b/ios/MullvadVPN/MutuallyExclusive.swift
@@ -13,13 +13,16 @@ extension Publishers {
/// A publisher that blocks the given DispatchQueue until the produced publisher reported the
/// completion.
- final class MutuallyExclusive<PublisherType, Context>: Publisher where PublisherType: Publisher, Context: Scheduler {
+ final class MutuallyExclusive<PublisherType, Context>: Publisher
+ where
+ PublisherType: Publisher,
+ Context: Scheduler
+ {
+ typealias MakePublisherBlock = () -> PublisherType
typealias Output = PublisherType.Output
typealias Failure = PublisherType.Failure
- typealias MakePublisherBlock = () -> PublisherType
-
private let exclusivityQueue: Context
private let executionQueue: Context
@@ -32,27 +35,100 @@ extension Publishers {
}
func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
- exclusivityQueue.schedule {
- let sema = DispatchSemaphore(value: 0)
- let releaseLock = {
- _ = sema.signal()
- }
+ let subscription = MutuallyExclusive.Subscription(
+ subscriber: subscriber,
+ createPublisher: createPublisher,
+ exclusivityQueue: exclusivityQueue,
+ executionQueue: executionQueue)
+
+ subscriber.receive(subscription: subscription)
+ }
+ }
+}
+
+private extension Publishers.MutuallyExclusive {
+
+ /// A subscription used by `MutuallyExclusive` publisher
+ final class Subscription<SubscriberType, PublisherType, Context>: Combine.Subscription
+ where
+ SubscriberType: Subscriber, PublisherType: Publisher,
+ PublisherType.Output == SubscriberType.Input,
+ PublisherType.Failure == SubscriberType.Failure,
+ Context: Scheduler
+ {
+ typealias MakePublisherBlock = () -> PublisherType
+
+ private let subscriber: SubscriberType
+ private var innerSubscriber: AnyCancellable?
+ private let createPublisher: MakePublisherBlock
+
+ private let exclusivityQueue: Context
+ private let executionQueue: Context
+ private let sema = DispatchSemaphore(value: 0)
+
+ private let cancelLock = NSLock()
+ private var isCancelled = false
+
+ init(subscriber: SubscriberType,
+ createPublisher: @escaping MakePublisherBlock,
+ exclusivityQueue: Context,
+ executionQueue: Context)
+ {
+ self.subscriber = subscriber
+ self.createPublisher = createPublisher
+ self.exclusivityQueue = exclusivityQueue
+ self.executionQueue = executionQueue
+ }
+ func request(_ demand: Subscribers.Demand) {
+ self.exclusivityQueue.schedule {
self.executionQueue.schedule {
- self.createPublisher()
- .handleEvents(receiveCompletion: { _ in
- releaseLock()
- }, receiveCancel: {
- releaseLock()
- })
- .subscribe(subscriber)
+ self.cancelLock.withCriticalBlock {
+ guard !self.isCancelled else { return }
+
+ self.innerSubscriber = self.createPublisher()
+ .sink(receiveCompletion: { [weak self] (completion) in
+ guard let self = self else { return }
+
+ self.subscriber.receive(completion: completion)
+ self.signalSemaphore()
+ }, receiveValue: { [weak self] (output) in
+ _ = self?.subscriber.receive(output)
+ })
+ }
}
+ self.sema.wait()
+ }
+ }
+
+ func cancel() {
+ cancelLock.withCriticalBlock {
+ guard !isCancelled else { return }
- sema.wait()
+ isCancelled = true
+
+ innerSubscriber?.cancel()
+ innerSubscriber = nil
+
+ signalSemaphore()
}
}
+
+ private func signalSemaphore() {
+ _ = sema.signal()
+ }
+
}
}
+private extension NSLock {
+ func withCriticalBlock<T>(_ body: () -> T) -> T {
+ lock()
+ defer { unlock() }
+
+ return body()
+ }
+}
+
typealias MutuallyExclusive = Publishers.MutuallyExclusive
diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift
index c80356da11..4c8fefbd92 100644
--- a/ios/MullvadVPN/RelayCache.swift
+++ b/ios/MullvadVPN/RelayCache.swift
@@ -15,8 +15,8 @@ enum RelayCacheError: Error {
case defaultLocationNotFound
case io(Error)
case coding(Error)
- case network(MullvadAPIError)
- case server(JsonRpcResponseError)
+ case network(MullvadAPI.Error)
+ case server(JsonRpcResponseError<MullvadAPI.ResponseCode>)
}
/// A enum describing the source of the relay list
diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift
index 9ea4ab98f9..f1b0ab8c59 100644
--- a/ios/MullvadVPN/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager.swift
@@ -33,6 +33,12 @@ enum TunnelManagerError: Error {
/// A failure to get the relay constraints
case getRelayConstraints(TunnelConfigurationManagerError)
+
+ /// A failure to get a public key used for Wireguard
+ case getWireguardPublicKey(TunnelConfigurationManagerError)
+
+ /// A failure to re-generate a private key used for Wireguard
+ case regenerateWireguardPrivateKey(RegenerateWireguardPrivateKeyError)
}
enum TunnelIpcRequestError: Error {
@@ -63,7 +69,7 @@ enum SetAccountError: Error {
/// A failure to push the wireguard key
case pushWireguardKey(PushWireguardKeyError)
- /// A failure to set up the tunnel
+ /// A failure to set up a tunnel
case setup(SetupTunnelError)
}
@@ -75,9 +81,23 @@ enum UnsetAccountError: Error {
case removeTunnelConfiguration(TunnelConfigurationManagerError)
}
+enum RegenerateWireguardPrivateKeyError: Error {
+ /// A failure to read the public Wireguard key from Keychain
+ case readPublicWireguardKey(TunnelConfigurationManagerError)
+
+ /// A failure to replace the public Wireguard key
+ case replaceWireguardKey(PushWireguardKeyError)
+
+ /// A failure to update tunnel configuration
+ case updateTunnelConfiguration(UpdateTunnelConfigurationError)
+
+ /// A failure to set up a tunnel
+ case setupTunnel(SetupTunnelError)
+}
+
enum PushWireguardKeyError: Error {
- case network(MullvadAPIError)
- case server(JsonRpcResponseError)
+ case transport(MullvadAPI.Error)
+ case server(MullvadAPI.ResponseError)
}
enum UpdateTunnelConfigurationError: Error {
@@ -320,12 +340,12 @@ class TunnelManager {
}
// Send wireguard key to the server
- let publicKey = tunnelConfig.interface.privateKey.publicKeyRawRepresentation
+ let publicKey = tunnelConfig.interface.privateKey.publicKey.rawRepresentation
return self.apiClient.pushWireguardKey(accountToken: accountToken, publicKey: publicKey)
.mapError { (networkError) -> SetAccountError in
- return .pushWireguardKey(.network(networkError))
- }.flatMap { (response: JsonRpcResponse<WireguardAssociatedAddresses>) in
+ return .pushWireguardKey(.transport(networkError))
+ }.flatMap { (response: MullvadAPI.Response<WireguardAssociatedAddresses>) in
return response.result.publisher
.mapError { (serverError) -> SetAccountError in
return .pushWireguardKey(.server(serverError))
@@ -399,6 +419,67 @@ class TunnelManager {
}.eraseToAnyPublisher()
}
+ func regeneratePrivateKey() -> AnyPublisher<(), TunnelManagerError> {
+ MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) {
+ Just(self.accountToken)
+ .setFailureType(to: TunnelManagerError.self)
+ .replaceNil(with: .missingAccount)
+ .flatMap { (accountToken) -> AnyPublisher<(), TunnelManagerError> in
+ let newPrivateKey = WireguardPrivateKey()
+
+ return TunnelConfigurationManager.load(account: accountToken)
+ .map { $0.interface.privateKey.publicKey }
+ .mapError { RegenerateWireguardPrivateKeyError.readPublicWireguardKey($0) }
+ .publisher
+ .flatMap { (oldPublicKey) in
+ self.apiClient.replaceWireguardKey(
+ accountToken: accountToken,
+ oldPublicKey: oldPublicKey.rawRepresentation,
+ newPublicKey: newPrivateKey.publicKey.rawRepresentation)
+ .mapError { (networkError) -> RegenerateWireguardPrivateKeyError in
+ return .replaceWireguardKey(.transport(networkError))
+ }.receive(on: self.executionQueue)
+ .flatMap { (response: MullvadAPI.Response<WireguardAssociatedAddresses>) in
+ return response.result.publisher
+ .mapError { (serverError) -> RegenerateWireguardPrivateKeyError in
+ return .replaceWireguardKey(.server(serverError))
+ }
+ }
+ .flatMap { (addresses) in
+ self.updateTunnelConfiguration(accountToken: accountToken) {
+ (tunnelConfiguration) in
+ tunnelConfiguration.interface.privateKey = newPrivateKey
+ tunnelConfiguration.interface.addresses = [
+ addresses.ipv4Address,
+ addresses.ipv6Address
+ ]
+ }
+ .mapError { .updateTunnelConfiguration($0) }
+ .publisher
+ }
+ .flatMap {
+ self.setupTunnel(accountToken: accountToken)
+ .map { _ in () }
+ .mapError { RegenerateWireguardPrivateKeyError.setupTunnel($0) }
+ }.receive(on: self.executionQueue)
+ .flatMap { _ in
+ // Ignore Packet Tunnel IPC errors but log them
+ self.reloadPacketTunnelConfiguration()
+ .replaceError(with: ())
+ .setFailureType(to: RegenerateWireguardPrivateKeyError.self)
+ .handleEvents(receiveCompletion: { (completion) in
+ if case .failure(let error) = completion {
+ os_log(.error, "Failed to tell the tunnel to reload configuration: %{public}s", error.localizedDescription)
+ }
+ })
+ }
+ }
+ .mapError { TunnelManagerError.regenerateWireguardPrivateKey($0) }
+ .eraseToAnyPublisher()
+ }
+ }.eraseToAnyPublisher()
+ }
+
func setRelayConstraints(_ constraints: RelayConstraints) -> AnyPublisher<(), TunnelManagerError> {
MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) {
Just(self.accountToken)
@@ -444,6 +525,19 @@ class TunnelManager {
}.eraseToAnyPublisher()
}
+ func getWireguardPublicKey() -> AnyPublisher<WireguardPublicKey, TunnelManagerError> {
+ MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) {
+ Just(self.accountToken)
+ .setFailureType(to: TunnelManagerError.self)
+ .replaceNil(with: .missingAccount)
+ .flatMap { (accountToken) in
+ TunnelConfigurationManager.load(account: accountToken)
+ .map { $0.interface.privateKey.publicKey }
+ .mapError { TunnelManagerError.getWireguardPublicKey($0) }.publisher
+ }
+ }.eraseToAnyPublisher()
+ }
+
// MARK: - Private
/// Tell Packet Tunnel process to reload the tunnel configuration
diff --git a/ios/MullvadVPN/WireguardPrivateKey.swift b/ios/MullvadVPN/WireguardPrivateKey.swift
index 781e4f3917..058055e77f 100644
--- a/ios/MullvadVPN/WireguardPrivateKey.swift
+++ b/ios/MullvadVPN/WireguardPrivateKey.swift
@@ -12,43 +12,72 @@ import Foundation
/// A convenience wrapper around the wireguard key
struct WireguardPrivateKey {
- /// An inner impelementation of a private key
- private let innerPrivateKey: CryptoKit.Curve25519.KeyAgreement.PrivateKey
+ /// When the key was created
+ let creationDate: Date
/// Private key's raw representation
var rawRepresentation: Data {
- return innerPrivateKey.rawRepresentation
+ innerPrivateKey.rawRepresentation
}
- /// Public key's raw representation
- var publicKeyRawRepresentation: Data {
- return innerPrivateKey.publicKey.rawRepresentation
+ /// Public key
+ var publicKey: WireguardPublicKey {
+ WireguardPublicKey(
+ creationDate: creationDate,
+ rawRepresentation: innerPrivateKey.publicKey.rawRepresentation
+ )
}
+ /// An inner impelementation of a private key
+ private let innerPrivateKey: Curve25519.KeyAgreement.PrivateKey
+
/// Initialize the new private key
init() {
- innerPrivateKey = CryptoKit.Curve25519.KeyAgreement.PrivateKey()
+ innerPrivateKey = Curve25519.KeyAgreement.PrivateKey()
+ creationDate = Date()
}
/// Load with the existing private key
- init(rawRepresentation: Data) throws {
- innerPrivateKey = try CryptoKit.Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation)
+ init(rawRepresentation: Data, createdAt: Date) throws {
+ innerPrivateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation)
+ creationDate = createdAt
}
}
+extension WireguardPrivateKey: Equatable {
+ static func == (lhs: WireguardPrivateKey, rhs: WireguardPrivateKey) -> Bool {
+ lhs.rawRepresentation == rhs.rawRepresentation
+ }
+}
+
+/// A struct holding a public key used for Wireguard with associated metadata
+struct WireguardPublicKey {
+ /// Refers to private key creation date
+ let creationDate: Date
+
+ /// Raw public key representation
+ let rawRepresentation: Data
+}
+
extension WireguardPrivateKey: Codable {
+
+ private enum CodingKeys: String, CodingKey {
+ case privateKeyData, creationDate
+ }
+
func encode(to encoder: Encoder) throws {
- var container = encoder.singleValueContainer()
+ var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(innerPrivateKey.rawRepresentation)
+ try container.encode(innerPrivateKey.rawRepresentation, forKey: .privateKeyData)
+ try container.encode(creationDate, forKey: .creationDate)
}
init(from decoder: Decoder) throws {
- let container = try decoder.singleValueContainer()
-
- let privateKeyBytes = try container.decode(Data.self)
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ let privateKeyBytes = try container.decode(Data.self, forKey: .privateKeyData)
+ let creationDate = try container.decode(Date.self, forKey: .creationDate)
- self = try .init(rawRepresentation: privateKeyBytes)
+ self = try .init(rawRepresentation: privateKeyBytes, createdAt: creationDate)
}
}