summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/Keychain/Keychain.swift
blob: 383219990afa5aad46df05bd8c6003832f0c1b3c (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
//
//  Keychain.swift
//  MullvadVPN
//
//  Created by pronebird on 22/04/2020.
//  Copyright © 2020 Mullvad VPN AB. All rights reserved.
//

import Foundation
import Security

protocol KeychainAttributeDecodable {
    init?(attributes: [CFString: Any])
}

protocol KeychainAttributeEncodable {
    func keychainRepresentation() -> [CFString: Any]
    func updateKeychainAttributes(in attributes: inout [CFString: Any])
}

extension KeychainAttributeEncodable {
    func keychainRepresentation() -> [CFString: Any] {
        var attributes = [CFString: Any]()
        updateKeychainAttributes(in: &attributes)
        return attributes
    }
}

enum Keychain {}

extension Keychain {

    /// A Keychain Result type
    typealias Result<T> = Swift.Result<T, Keychain.Error>

    static func add(_ attributes: Keychain.Attributes) -> Result<Keychain.Attributes?> {
        var result: CFTypeRef?
        let status = SecItemAdd(attributes.keychainRepresentation() as CFDictionary, &result)

        return mapSecResultAndReturnValue(
            status: status,
            value: result,
            returnSet: attributes.return ?? [],
            limit: .one)
            .map { $0.first }
    }

    static func update(query: Keychain.Attributes, update: Keychain.Attributes) -> Result<()> {
        let queryAttributes = query.keychainRepresentation() as CFDictionary
        let updateAttributes = update.keychainRepresentation() as CFDictionary

        let status = SecItemUpdate(queryAttributes, updateAttributes)

        return mapSecResult(status: status) {
            return ()
        }
    }

    static func delete(query: Keychain.Attributes) -> Result<()> {
        let status = SecItemDelete(query.keychainRepresentation() as CFDictionary)

        return mapSecResult(status: status) {
            return ()
        }
    }

    static func findFirst(query: Keychain.Attributes) -> Result<Keychain.Attributes?> {
        return find(query: query).map { $0.first }
    }

    static func find(query: Keychain.Attributes) -> Result<[Keychain.Attributes]> {
        let attributes = query.keychainRepresentation()

        var result: CFTypeRef?
        let status = SecItemCopyMatching(attributes as CFDictionary, &result)

        return mapSecResultAndReturnValue(
            status: status,
            value: result,
            returnSet: query.return ?? [],
            limit: query.matchLimit ?? .one
        )
    }

    static private func mapSecResultAndReturnValue(
        status: OSStatus,
        value: CFTypeRef?,
        returnSet: Set<Keychain.Return>,
        limit: Keychain.MatchLimit) -> Result<[Keychain.Attributes]>
    {
        return mapSecResult(status: status) { () -> [Keychain.Attributes] in
            return value.map { parseReturnValue(value: $0, returnSet: returnSet, limit: limit) }
                ?? []
        }
    }

    static private func parseReturnValue(
        value: CFTypeRef,
        returnSet: Set<Keychain.Return>,
        limit: Keychain.MatchLimit) -> [Keychain.Attributes]
    {
        switch returnSet {
        case []:
            return []

        case [.data]:
            let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit)

            return values.map { (data) -> Keychain.Attributes in
                var attributes = Keychain.Attributes()
                attributes.valueData = data
                return attributes
            }

        case [.persistentReference]:
            let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit)

            return values.map { (persistentReference) -> Keychain.Attributes in
                var attributes = Keychain.Attributes()
                attributes.valuePersistentReference = persistentReference
                return attributes
            }

        default:
            let rawAttributeList: [[CFString: Any]] =
                unsafelyCastReturnValue(value: value, limit: limit)

            return rawAttributeList.map { Keychain.Attributes(attributes: $0) }
        }
    }

    /// A private helper that casts and normalizes the return value from Keychain to produce
    /// an array even when a single item is expected to be returned.
    static private func unsafelyCastReturnValue<T>(
        value: CFTypeRef,
        limit: Keychain.MatchLimit) -> [T]
    {
        switch limit {
        case .one:
            return [value as! T]
        case .all:
            return value as! [T]
        }
    }

    /// A private helper that verifies the given `status` and executes `body` on success
    static private func mapSecResult<T>(status: OSStatus, body: () -> T) -> Result<T> {
        if status == errSecSuccess {
            return .success(body())
        } else {
            return .failure(Keychain.Error(code: status))
        }
    }
}