Revision control

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
import Foundation
6
import Account
7
import Shared
8
import FxA
9
import SwiftyJSON
10
11
/**
12
* Swift can't do functional factories. I would like to have one of the following
13
* approaches be viable:
14
*
15
* 1. Derive the constructor from the consumer of the factory.
16
* 2. Accept a type as input.
17
*
18
* Neither of these are viable, so we instead pass an explicit constructor closure.
19
*
20
* Most of these approaches produce either odd compiler errors, or -- worse --
21
* compile and then yield runtime EXC_BAD_ACCESS (see Radar 20230159).
22
*
23
* For this reason, be careful trying to simplify or improve this code.
24
*/
25
public func keysPayloadFactory<T: CleartextPayloadJSON>(keyBundle: KeyBundle, _ f: @escaping (JSON) -> T) -> (String) -> T? {
26
return { (payload: String) -> T? in
27
let potential = EncryptedJSON(json: payload, keyBundle: keyBundle)
28
if !potential.isValid() {
29
return nil
30
}
31
32
let cleartext = potential.cleartext
33
if cleartext == nil {
34
return nil
35
}
36
return f(cleartext!)
37
}
38
}
39
40
// TODO: how much do we want to move this into EncryptedJSON?
41
public func keysPayloadSerializer<T: CleartextPayloadJSON>(keyBundle: KeyBundle, _ f: @escaping (T) -> JSON) -> (Record<T>) -> JSON? {
42
return { (record: Record<T>) -> JSON? in
43
let json = f(record.payload)
44
if json.isNull() {
45
// This should never happen, but if it does, we don't want to leak this
46
// record to the server!
47
return nil
48
}
49
// Get the most basic kind of encoding: no pretty printing.
50
// This can throw; if so, we return nil.
51
// `rawData` simply calls JSONSerialization.dataWithJSONObject:options:error, which
52
// guarantees UTF-8 encoded output.
53
54
guard let bytes: Data = try? json.rawData(options: []) else { return nil }
55
56
// Given a valid non-null JSON object, we don't ever expect a round-trip to fail.
57
assert(!JSON(bytes).isNull())
58
59
// We pass a null IV, which means "generate me a new one".
60
// We then include the generated IV in the resulting record.
61
if let (ciphertext, iv) = keyBundle.encrypt(bytes, iv: nil) {
62
// So we have the encrypted payload. Now let's build the envelope around it.
63
let ciphertext = ciphertext.base64EncodedString
64
65
// The HMAC is computed over the base64 string. As bytes. Yes, I know.
66
if let encodedCiphertextBytes = ciphertext.data(using: .ascii, allowLossyConversion: false) {
67
let hmac = keyBundle.hmacString(encodedCiphertextBytes)
68
let iv = iv.base64EncodedString
69
70
// The payload is stringified JSON. Yes, I know.
71
let payload: Any = JSON(["ciphertext": ciphertext, "IV": iv, "hmac": hmac]).stringify()! as Any
72
let obj = ["id": record.id,
73
"sortindex": record.sortindex,
74
// This is how SwiftyJSON wants us to express a null that we want to
75
// serialize. Yes, this is gross.
76
"ttl": record.ttl ?? NSNull(),
77
"payload": payload]
78
return JSON(obj)
79
}
80
}
81
return nil
82
}
83
}
84
85
open class Keys: Equatable {
86
let valid: Bool
87
let defaultBundle: KeyBundle
88
var collectionKeys: [String: KeyBundle] = [String: KeyBundle]()
89
90
public init(defaultBundle: KeyBundle) {
91
self.defaultBundle = defaultBundle
92
self.valid = true
93
}
94
95
public init(payload: KeysPayload?) {
96
if let payload = payload, payload.isValid(),
97
let keys = payload.defaultKeys {
98
self.defaultBundle = keys
99
self.collectionKeys = payload.collectionKeys
100
self.valid = true
101
return
102
}
103
self.defaultBundle = KeyBundle.invalid
104
self.valid = false
105
}
106
107
public convenience init(downloaded: EnvelopeJSON, master: KeyBundle) {
108
let f: (JSON) -> KeysPayload = { KeysPayload($0) }
109
let keysRecord = Record<KeysPayload>.fromEnvelope(downloaded, payloadFactory: keysPayloadFactory(keyBundle: master, f))
110
self.init(payload: keysRecord?.payload)
111
}
112
113
open class func random() -> Keys {
114
return Keys(defaultBundle: KeyBundle.random())
115
}
116
117
open func forCollection(_ collection: String) -> KeyBundle {
118
if let bundle = collectionKeys[collection] {
119
return bundle
120
}
121
return defaultBundle
122
}
123
124
open func encrypter<T>(_ collection: String, encoder: RecordEncoder<T>) -> RecordEncrypter<T> {
125
return RecordEncrypter(bundle: forCollection(collection), encoder: encoder)
126
}
127
128
open func asPayload() -> KeysPayload {
129
let json = JSON([
130
"id": "keys",
131
"collection": "crypto",
132
"default": self.defaultBundle.asPair(),
133
"collections": mapValues(self.collectionKeys, f: { $0.asPair() })
134
])
135
return KeysPayload(json)
136
}
137
138
public static func ==(lhs: Keys, rhs: Keys) -> Bool {
139
return lhs.valid == rhs.valid &&
140
lhs.defaultBundle == rhs.defaultBundle &&
141
lhs.collectionKeys == rhs.collectionKeys
142
}
143
}
144
145
/**
146
* Yup, these are basically typed tuples.
147
*/
148
public struct RecordEncoder<T: CleartextPayloadJSON> {
149
let decode: (JSON) -> T
150
let encode: (T) -> JSON
151
}
152
153
public struct RecordEncrypter<T: CleartextPayloadJSON> {
154
let serializer: (Record<T>) -> JSON?
155
let factory: (String) -> T?
156
157
init(bundle: KeyBundle, encoder: RecordEncoder<T>) {
158
self.serializer = keysPayloadSerializer(keyBundle: bundle, encoder.encode)
159
self.factory = keysPayloadFactory(keyBundle: bundle, encoder.decode)
160
}
161
162
init(serializer: @escaping (Record<T>) -> JSON?, factory: @escaping (String) -> T?) {
163
self.serializer = serializer
164
self.factory = factory
165
}
166
}
167