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 XCGLogger
10
import SwiftyJSON
11
12
private let log = Logger.syncLogger
13
14
/**
15
* Turns JSON of the form
16
*
17
* { ciphertext: ..., hmac: ..., iv: ...}
18
*
19
* into a new JSON object resulting from decrypting and parsing the ciphertext.
20
*/
21
open class EncryptedJSON {
22
var json: JSON
23
var _cleartext: JSON? // Cache decrypted cleartext.
24
var _ciphertextBytes: Data? // Cache decoded ciphertext.
25
var _hmacBytes: Data? // Cache decoded HMAC.
26
var _ivBytes: Data? // Cache decoded IV.
27
28
var valid: Bool = false
29
var validated: Bool = false
30
31
let keyBundle: KeyBundle
32
33
public init(json: String, keyBundle: KeyBundle) {
34
self.keyBundle = keyBundle
35
self.json = JSON(parseJSON: json)
36
}
37
38
public init(json: JSON, keyBundle: KeyBundle) {
39
self.keyBundle = keyBundle
40
self.json = json
41
}
42
43
// For validating HMAC: the raw ciphertext as bytes without decoding.
44
fileprivate var ciphertextB64: Data? {
45
if let ct = self["ciphertext"].string {
46
return Bytes.dataFromBase64(ct)
47
}
48
return nil
49
}
50
51
/**
52
* You probably want to call validate() and then use .ciphertext.
53
*/
54
fileprivate var ciphertextBytes: Data? {
55
return Bytes.decodeBase64(self["ciphertext"].string!)
56
}
57
58
fileprivate func validate() -> Bool {
59
if validated {
60
return valid
61
}
62
63
defer { validated = true }
64
65
guard self["ciphertext"].isString() &&
66
self["hmac"].isString() &&
67
self["IV"].isString() else {
68
valid = false
69
return false
70
}
71
72
guard let ciphertextForHMAC = self.ciphertextB64 else {
73
valid = false
74
return false
75
}
76
77
guard keyBundle.verify(hmac: self.hmac, ciphertextB64: ciphertextForHMAC) else {
78
valid = false
79
return false
80
}
81
82
// I guess we called validate twice…
83
if self._ciphertextBytes != nil {
84
valid = true
85
return true
86
}
87
88
// Also verify that the ciphertext is valid base64. Do this by
89
// retrieving the value in a failable way, leaving the accessors
90
// to take the dangerous/simple path.
91
// We can force-unwrap self["ciphertext"] because we already checked
92
// it when verifying the HMAC above.
93
guard let data = self.ciphertextBytes else {
94
log.error("Unable to decode ciphertext base64 in record \(self["id"].string ?? "<unknown>")")
95
valid = false
96
return false
97
}
98
99
self._ciphertextBytes = data
100
valid = true
101
return valid
102
}
103
104
open func isValid() -> Bool {
105
return !json.isError() && self.validate()
106
}
107
108
/**
109
* Make sure you call isValid first. This API force-unwraps for simplicity.
110
*/
111
var ciphertext: Data {
112
if _ciphertextBytes != nil {
113
return _ciphertextBytes!
114
}
115
116
_ciphertextBytes = self.ciphertextBytes
117
return _ciphertextBytes!
118
}
119
120
var hmac: Data {
121
if _hmacBytes != nil {
122
return _hmacBytes!
123
}
124
//NSData(base16EncodedString: self["hmac"].asString!, options: NSDataBase16DecodingOptions.Default)
125
_hmacBytes = NSData(base16EncodedString: self["hmac"].stringValue, options: []) as Data
126
return _hmacBytes!
127
}
128
129
var iv: Data {
130
if _ivBytes != nil {
131
return _ivBytes!
132
}
133
134
_ivBytes = Bytes.decodeBase64(self["IV"].string!)
135
return _ivBytes!
136
}
137
138
// Returns nil on error.
139
open var cleartext: JSON? {
140
if _cleartext != nil {
141
return _cleartext
142
}
143
144
if !isValid() {
145
log.error("Failed to validate.")
146
return nil
147
}
148
149
let decrypted: String? = keyBundle.decrypt(self.ciphertext, iv: self.iv)
150
if decrypted == nil {
151
log.error("Failed to decrypt.")
152
valid = false
153
return nil
154
}
155
156
_cleartext = JSON(parseJSON: decrypted!)
157
return _cleartext!
158
}
159
160
subscript(key: String) -> JSON {
161
get {
162
return json[key]
163
}
164
165
set {
166
json[key] = newValue
167
}
168
}
169
}