forked from IonicDev/ionic-devreq-api-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkey_create.py
More file actions
397 lines (339 loc) · 20.3 KB
/
key_create.py
File metadata and controls
397 lines (339 loc) · 20.3 KB
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
###########################################################
# This is a code sample for the Ionic Security Inc. API, #
# It assumes a SEP and usage v2.3 of the API #
# The intention is to show how to interact with the API #
# using built-in and 3rd-party libraries instead of the #
# Ionic SDK. #
# #
# This example uses Python 3.4.3 #
# This example is best read with syntax highlighting on. #
# #
# (c) 2017 Ionic Security Inc. #
# Confidential and Proprietary #
# By using this code, I agree to the Terms & Conditions #
# (https://www.ionic.com/terms-of-use/) and the Privacy #
# Policy (https://www.ionic.com/privacy-notice/) #
# Author = daniel/rmspeers, QA = jmassey #
###########################################################
import base64
import binascii
import hashlib
import json
import os
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import keys.utilities as utilities
####################################################
### Requires a Device Secure Enrollment Profile ###
####################################################
def encrypt_key_attribute(ionic_sep, encrypted_attributes_name, attribute_values_to_encrypt):
# To construct encrypted attributes:
## 1. Encode the original values as a JSON array in UTF-8.
## 2. Encrypt the encoded values using `SEP.CD:KS` key under 256-bit AES in GCM with the UTF-8 encoded attribute name used as
## additional authenticated data. The initialization vector should be prepended to the cipher text and the auth tag should
## be appended to the cipher text.
## 3. Encode the result using Base64.
## 4. Add the resulting string as the only value in the list under the attribute name.
# 1. Encode the original values as a JSON array in UTF-8.
json_array_of_attribute_values_to_encrypt = json.dumps(attribute_values_to_encrypt).encode(encoding='utf-8')
# 2. Encrypt the encoded values using `SEP.CD:KS` key under 256-bit AES in GCM with the UTF-8 encoded attribute name used as
# additional authenticated data. The initialization vector should be prepended to the cipher text and the auth tag should
# be appended to the cipher text.
# Create a 16-byte initialization vector of random bytes.
attributes_to_encrypt_initialization_vector = os.urandom(16)
# Create an AES-GCM cipher using the `SEP.CD:KS` as the key and the 16-byte initialization vector
cipher = Cipher(algorithms.AES(ionic_sep.aesCdEiKey),
modes.GCM(attributes_to_encrypt_initialization_vector),
backend=default_backend()
).encryptor()
# Set the authenticated data (AAD) to be the attributes name UTF-8 encoded
cipher.authenticate_additional_data(encrypted_attributes_name.encode(encoding='utf-8'))
# Encrypt the JSON array of attributes as UTF-8 bytes using 256-bit AES in GCM.
attributes_to_encrypt_cipher_text = cipher.update(json_array_of_attribute_values_to_encrypt) + cipher.finalize()
# Prepend the 16-byte initialization vector to the cipher text and append the auth tag to the cipher text.
## Prepend the resulting cipher text bytes with the initialization vector.
## Append the auth tag to the resulting cipher text.
attributes_to_encrypt_iv_cipher_text_aad_bytes = b''.join([attributes_to_encrypt_initialization_vector,
attributes_to_encrypt_cipher_text,
cipher.tag])
# 3. Encode the result using Base64
return base64.b64encode(attributes_to_encrypt_iv_cipher_text_aad_bytes)
def create_key_transaction(ionic_sep, dictKeyAttrs, dictMetadata, send_full_hfp=False):
###########################################
### Constructing the Key Create Request ###
###########################################
# NOTE: This type of request when encrypting attributes and performing attribute signing requires
# a conversation id (`cid`) to be used in AES-GCM encryption operations. We must construct the `cid`
# prior to performing any of these integrity ensuring cryptographic operations. Hence, the steps
# for constructing a device request as listed generally previously are modified here.
example_key_create_request_body = """
{
"cid": "CID|MfyG..A.ec095b70-c1d0-4ac0-9d0f-2cafa82b8a1f|1487622171374|1487622171374|5bFnTQ==",
"envelope": {
"meta": {
"hfphash": "aa0e43bfd6e7d5a9ea88e72d38d12df50bfb36e66710a5bb8417d8fb48230fc9",
},
"data": {
"protection-keys": [
{
"ref":"firstKeyType",
"qty":1,
"cattrs": "{\"attr1\":[\"attr1val1\",\"attr1val2\"],\"attr2\":[\"attr2val1\"]}",
"csig": "oRPW3N3a4CKdwhV00XlJKLZ/4GTjWJ+V4MJh2Ry3Z5g="
}
]
}
}
}
"""
# 1. Compose a Conversation ID string in a UTF-8 encoded byte array.
## Choose the appropriate SEP for this request.
cid = utilities.make_cid(ionic_sep.deviceId)
# 2. Construct the request data.
# The value is a JSON object with a field "protection-keys" containing
# an array of key creation objects.
# NOTE: This represents a very basic key create object.
ref1 = "refType1"
key_create_data = {
"protection-keys": [
{
"qty": 1,
"ref": ref1
}
]
}
# NOTE: The following shows how to add encrypted attributes to a key creation object. This must be performed before
# the next step of Signing Attributes because this data will be contained within the object constructed in the next
# section (Keys with Signed Attributes)
###################################
### BEGIN: Encrypted Attributes ###
###################################
# The device may encrypt attributes such that Ionic.com cannot see them in transition to the keyserver.
## NOTE: Such attributes cannot affect the policies applied to those keys as the policy server also cannot decrypt those attributes.
# The encrypted attributes should be namespaced with ionic-protected-.
## (ionic-integrity-hash is a special case that also is treated as an encrypted attribute.)
dictKeyAttrs_keysToEncrypt = filter(lambda x: x.startswith('ionic-protected-'), dictKeyAttrs.keys())
if 'ionic-integrity-hash' in dictKeyAttrs:
dictKeyAttrs_keysToEncrypt.append('ionic-integrity-hash')
# For each attribute where we want the values encrypted, perform the encryption and overwrite the plaintext value:
for encrypted_attribute_name in dictKeyAttrs_keysToEncrypt:
b64_encrypted_attributes = encrypt_key_attribute(ionic_sep, encrypted_attribute_name, dictKeyAttrs[encrypted_attribute_name])
dictKeyAttrs[encrypted_attribute_name] = [b64_encrypted_attributes.decode(encoding='utf-8')]
# We now have a field name and values to add to the attributes field (`cattrs`) constructed below
# We will perform step 4 during the final creation of the cattrs object.
#################################
### END: Encrypted Attributes ###
#################################
# NOTE: The follow shows how to create a key with signed attributes to ensure integrity of attribute data.
##########################################
### BEGIN: Keys with Signed Attributes ###
##########################################
# Keys with Signed Attributes
## 1. Prepare the required attributes. Each attribute contains a name and a list of values.
## 2. Encode the attributes into a JSON object encoded in UTF-8 bytes.
## 3. Compute the SHA256 hash of those bytes.
## 4. Form the authenticated data as the UTF-8 representation of the ref field appended to the Conversation ID
## separated by a colon (:), like CID|Mfyg...|..4b:refID1.
## 5. Encrypt the attribute hash bytes using 256-bit AES in GCM.
### - Construct a nonce 16-byte initialization vector.
### - Use the additional authenticated data constructed from the prior step.
### - The key to be used is the SEP.CD:KS key.
## 6. Prepend the 16-byte initialization vector to the cipher text and append the auth tag to the cipher text.
## 7. Base64-encode the combined initialization vector and cipher text bytes.
# 1a. Attributes should be set by the caller of this function, such as:
#dictKeyAttrs = {"keyType1AttributeField1": ["keyType1AttributeValue1"]}
# 1b. We can add an external ID into this list. If desired, the caller of this function should set it such as:
#dictKeyAttrs["ionic-external-id"] = ["exampleExternalID"]
# The field name and value of the Encrypted Attributes were already placed here in the above code.
# The data sent to Ionic will be the stringified `cattrs` object
cattrs_as_string = json.dumps(dictKeyAttrs)
# 2. Encode the attributes into a JSON object encoded in UTF-8 bytes.
cattrs_as_bytes = cattrs_as_string.encode(encoding='utf-8')
# 3. Compute the SHA256 hash of those bytes.
sha256er = hashlib.sha256()
sha256er.update(cattrs_as_bytes)
cattr_sha256_bytes = sha256er.digest()
# 4. Form the authenticated data as the UTF-8 representation of the ref field appended to the Conversation ID
# separated by a colon (:), like CID|Mfyg...|..4b:refID1.
signed_attributes_aad = ':'.join([cid, ref1])
# 5. Encrypt the attribute hash bytes using 256-bit AES in GCM.
# - Construct a nonce 16-byte initialization vector.
signed_attributes_initialization_vector = os.urandom(16)
# Create an AES-GCM cipher using the SEP.CD:KS as the key and the 16-byte initialization vector
# - The key to be used is the `SEP.CD:KS` key
cipher = Cipher(algorithms.AES(ionic_sep.aesCdEiKey),
modes.GCM(signed_attributes_initialization_vector),
backend=default_backend()
).encryptor()
# - Use the additional authenticated data constructed from the prior step.
## Set the authenticated data (AAD) to be the signed attributes AAD (<CID>|<refType>)
cipher.authenticate_additional_data(signed_attributes_aad.encode(encoding='utf-8'))
signed_attributes_cipher_text = cipher.update(cattr_sha256_bytes) + cipher.finalize()
# 6. Prepend the 16-byte initialization vector to the cipher text and append the auth tag to the cipher text.
signed_attributes_iv_cipher_text_aad = b''.join([signed_attributes_initialization_vector,
signed_attributes_cipher_text,
cipher.tag])
# 7. Base64-encode the combined initialization vector and cipher text bytes.
b64encoded_signed_attributes_iv_cipher_text_aad_as_string = base64.b64encode(signed_attributes_iv_cipher_text_aad).decode(encoding='utf-8')
# Add the attributes and signature to the key creation object
# `cattrs` is the stringified object
key_create_data['protection-keys'][0]['cattrs'] = cattrs_as_string
key_create_data['protection-keys'][0]['csig'] = b64encoded_signed_attributes_iv_cipher_text_aad_as_string
########################################
### END: Keys with Signed Attributes ###
########################################
# 3. Construct or obtain a cached copy of the needed meta data.
## - This will include the hfphash.
## - If an update is needed, this should include the full hfp.
# NOTE: This represents the minimum contents, and it assumes that device's fingerprint is saved in IDC and has not changed locally.
# Therefore we can send only the `hfphash`.
# Here the `hfphash` is obtained from the `ionic_sep` dict:
hfphash = ionic_sep.get_hfp_hash()
# 4. Compose the `meta` and `data`. Serialize the JSON object containing `meta` and `data` as fields to a byte array
# representation encoded using UTF-8.
# Construct the `envelope` contents: { "meta": <>, "data": <> }
if send_full_hfp:
hfp = ionic_sep.get_hfp()
# this will convert the hfp from dict to string. The server expects a string and not a json object
if isinstance(hfp, dict):
hfp = json.dumps(hfp)
envelope_contents = {
"meta": {
"hfphash": hfphash,
"hfp": hfp
},
"data" : key_create_data
}
else:
envelope_contents = {
"meta": {
"hfphash": hfphash
},
"data" : key_create_data
}
# Serialize the `envelope` contents to a byte array containing the UTF-8 encoding of the contents
serialized_envelope_contents = json.dumps(envelope_contents).encode(encoding='utf-8')
# 5. Encrypt the array of JSON bytes using AES-256 GCM. AES-256 GCM encryption requires a key, an initialization vector,
# and auth data (also called AAD). Encryption returns both encrypted bytes and an auth tag.
## - Use the SEP.CD:IDC value as the key.
## - Create a 16-byte initialization vector of random bytes.
## - Set the authenticated data (AAD) to be the Conversation ID byte array.
## - Encrypt the JSON byte array.
# Create a 16-byte initialization vector of random bytes.
initialization_vector = os.urandom(16)
# Create an AES-GCM cipher using the SEP.CD:IDC as the key and the 16-byte initialization vector
cipher = Cipher(algorithms.AES(ionic_sep.aesCdIdcKey),
modes.GCM(initialization_vector),
backend=default_backend()
).encryptor()
# Set the authenticated data (AAD) to be the Conversation ID byte array.
cipher.authenticate_additional_data(cid.encode(encoding='utf-8'))
# Encrypt the JSON byte array.
cipher_text = cipher.update(serialized_envelope_contents) + cipher.finalize()
# 6. Combine the initialization vector, the encrypted JSON, and the auth tag.
iv_cipher_text_aad = b''.join([initialization_vector,
cipher_text,
cipher.tag])
# 7. Encode the results of the previous step as a single array of bytes using Base64.
b64encoded_iv_cipher_text_aad_as_string = base64.b64encode(iv_cipher_text_aad).decode(encoding='utf-8')
# 8. Compose a JSON representation containing the resulting Base64-encoded string as the value
# of the envelope field and the Conversation ID string as the value of the cid field.
# This representation should use UTF-8 encoding.
key_create_request_body = {
"cid": cid,
"envelope": b64encoded_iv_cipher_text_aad_as_string
}
# Send the request to Ionic as an HTTP POST with JSON data to https://dev-api.ionic.com/v2.3/keys/create
print('Creating keys: {}'.format(key_create_data))
key_create_response = requests.post('%s/v2.3/keys/create' % ionic_sep.server,
data=json.dumps(key_create_request_body),
headers={'Content-Type': 'application/json'})
########################################
### Handling the Key Create Response ###
########################################
# Assume the response from Ionic is a successful 200, and we have created keys with the provided attributes.
assert (key_create_response.status_code == 200) or (key_create_response.status_code == 401)
return key_create_response, cid, b64encoded_signed_attributes_iv_cipher_text_aad_as_string
def create_keys(ionic_sep, dictKeyAttrs = {}, dictMetadata = {}):
key_create_response, cid, b64encoded_signed_attributes_iv_cipher_text_aad_as_string = create_key_transaction(ionic_sep, dictKeyAttrs, dictMetadata)
decrypted_envelope, response_cid = utilities.decrypt_envelope(ionic_sep, key_create_response, cid)
# NOTE: It is possible that an error occurred, perhaps the time recorded by the client and that held by the server differ
# more than +/-5 minutes or the device needs to send its entire fingerprint (`hfp`).
# Please see the errors and error handling for more information.
if decrypted_envelope.get('error'):
print("\nA partial error has occurred. The following is the response from the server:")
print(decrypted_envelope["error"])
if decrypted_envelope["error"]["code"] == 4001:
print("\nIn this scenario, the hfphash is not recognized by the server. A new request will be generated with "
"the full HFP included.")
key_create_response, cid, b64encoded_signed_attributes_iv_cipher_text_aad_as_string = create_key_transaction(ionic_sep, dictKeyAttrs, dictMetadata, send_full_hfp=True)
decrypted_envelope, response_cid = utilities.decrypt_envelope(ionic_sep, key_create_response, cid)
example_key_create_response_keys = """
{
"cid": "CID|MfyG..A.ec095b70-c1d0-4ac0-9d0f-2cafa82b8a1f|1487622171374|1487622171374|5bFnTQ==",
"envelope": {
"data": {
"protection-keys": [
{
"ref":"firstKeyType",
"id": "MfygGadsg23",
"key":"ad01f035b2b45967f71099ca29893381675904941bd91a72a0044e9e3087eaf2bafa07b6e106361a3acb1e2eb6e191e5fad50690f0b97871414b256e496aed38"
},
{
"ref":"secondKeyType",
"id": "MfygGP34erq",
"key":"f980333fb2b6da08f7e02b9d8e5f2b3f2c119777962c92f02b8a19276766fe675b26773ca69c417e5f957a7e58922d1e7da28f9349f35eac8afb5d35fd83bc69"
},
{
"ref":"secondKeyType",
"id": "MfygGP2Dg19",
"key":"c2221c814c5a1acacde0671c3b5e53786ec67851da7ca2455ab7640b7dd02b0d50a1706bb6bb80fe8baf05ec796f45532cb3a808c39a9d7669bb2c169db576f8"
},
{
"ref":"secondKeyType",
"id": "Mfygm6sdzcx",
"key":"e7ee8f671d73aff9c02ff002e2732ddea5f3716bab98ccbdd7514bb4b32d1ecd67209ad1127ce22b10a0d190448ea285e71dcd068bee8d5b647a9ffbe9342a27"
}
]
}
}
}
"""
# To decrypt each returned data key:
## 1. Decode the contents of the `key` field using hex. The cipher text contains the initialization vector,
# the data, and the auth tag.
## 2. Form the additional authenticated data for the key.
### - This is the UTF-8 representation of the Conversation ID, `ref` field, and
### `id` field separated by colons (`:`), like `CID|Mfyg...|..4b:firstKeyType:MfygGadsg23`.
### - If the attributes for that key were signed, the Base64-encoded signature
### field from the request is also included in the auth data, like
### `CID|Mfyg...|..4b:firstKeyType:MfygGadsg23:oRPW3N3a4...MJh2Ry3Z5g=`.
## 3. Decrypt the decoded key bytes using 256-bit AES GCM.
### - The initialization vector is the first 16 bytes of the decoded bytes.
### - The auth tag is the last 16 bytes of the decoded bytes.
### - The key is the `SEP.CD:KS` key.
### - The additional authenticated data are the bytes formed in the previous step.
## The result is a 256-bit AES key in raw bytes.
decrypted_keys = {} # simple storage for the data we're decrypting from the response
for key in decrypted_envelope['data']['protection-keys']:
key_id = key['id']
key_ref = key['ref']
aad = ':'.join([response_cid, key_ref, key_id, b64encoded_signed_attributes_iv_cipher_text_aad_as_string])
hex_encoded_encrypted_key = key['key']
encrypted_key_bytes = binascii.unhexlify(hex_encoded_encrypted_key)
key_iv = encrypted_key_bytes[:16]
key_data = encrypted_key_bytes[16:-16]
key_auth_tag = encrypted_key_bytes[-16:]
cipher = Cipher(algorithms.AES(ionic_sep.aesCdEiKey),
modes.GCM(key_iv,
key_auth_tag),
backend=default_backend()
).decryptor()
cipher.authenticate_additional_data(aad.encode(encoding='utf-8'))
decrypted_key_bytes = cipher.update(key_data) + cipher.finalize()
# The ref can be used for grouping types of keys with similar attributes
decrypted_keys[key_id] = key
decrypted_keys[key_id]['ref'] = key_ref
decrypted_keys[key_id]['key'] = decrypted_key_bytes
return decrypted_keys