ppldid 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS +1 -0
  3. data/LICENSE +201 -0
  4. data/README.md +2 -0
  5. data/VERSION +1 -0
  6. data/lib/oydid/basic.rb +278 -0
  7. data/lib/oydid/didcomm.rb +120 -0
  8. data/lib/oydid/log.rb +394 -0
  9. data/lib/oydid.rb +738 -0
  10. data/spec/input/basic/arrays.json +8 -0
  11. data/spec/input/basic/french.json +6 -0
  12. data/spec/input/basic/sample2_get_location.doc +1 -0
  13. data/spec/input/basic/sample2_retrieve_document.doc +1 -0
  14. data/spec/input/basic/sample3_retrieve_document.doc +1 -0
  15. data/spec/input/basic/sample4_retrieve_document.doc +1 -0
  16. data/spec/input/basic/sample5_retrieve_document.doc +1 -0
  17. data/spec/input/basic/sample_dec.doc +1 -0
  18. data/spec/input/basic/sample_enc.doc +1 -0
  19. data/spec/input/basic/sample_get_location.doc +1 -0
  20. data/spec/input/basic/sample_hash.doc +1 -0
  21. data/spec/input/basic/sample_invalid2_readkey.doc +1 -0
  22. data/spec/input/basic/sample_invalid2_verify.doc +1 -0
  23. data/spec/input/basic/sample_invalid3_readkey.doc +1 -0
  24. data/spec/input/basic/sample_invalid3_verify.doc +1 -0
  25. data/spec/input/basic/sample_invalid_privkey.doc +1 -0
  26. data/spec/input/basic/sample_invalid_readkey.doc +1 -0
  27. data/spec/input/basic/sample_invalid_sign.doc +1 -0
  28. data/spec/input/basic/sample_invalid_verify.doc +1 -0
  29. data/spec/input/basic/sample_key.doc +1 -0
  30. data/spec/input/basic/sample_readkey.doc +1 -0
  31. data/spec/input/basic/sample_retrieve_document.doc +1 -0
  32. data/spec/input/basic/sample_sign.doc +1 -0
  33. data/spec/input/basic/sample_valid_privkey.doc +1 -0
  34. data/spec/input/basic/sample_verify.doc +1 -0
  35. data/spec/input/basic/structures.json +8 -0
  36. data/spec/input/basic/unicode.json +3 -0
  37. data/spec/input/basic/values.json +5 -0
  38. data/spec/input/basic/wierd.json +11 -0
  39. data/spec/input/basic/zQmaBZTghn.doc +1 -0
  40. data/spec/input/log/sample0_dag2array.doc +1 -0
  41. data/spec/input/log/sample0_dag_did.doc +1 -0
  42. data/spec/input/log/sample1_dag_did.doc +1 -0
  43. data/spec/input/log/sample1_dag_update.doc +1 -0
  44. data/spec/input/log/sample2_dag_did.doc +1 -0
  45. data/spec/input/log/sample2_dag_update.doc +1 -0
  46. data/spec/input/log/sample2_retrieve_log.doc +1 -0
  47. data/spec/input/log/sample3_dag_did.doc +1 -0
  48. data/spec/input/log/sample3_dag_update.doc +1 -0
  49. data/spec/input/log/sample3_retrieve_log.doc +1 -0
  50. data/spec/input/log/sample4_dag_did.doc +1 -0
  51. data/spec/input/log/sample4_dag_update.doc +1 -0
  52. data/spec/input/log/sample4_retrieve_log.doc +1 -0
  53. data/spec/input/log/sample5_dag_update.doc +1 -0
  54. data/spec/input/log/sample5_retrieve_log.doc +1 -0
  55. data/spec/input/log/sample6_dag_update.doc +1 -0
  56. data/spec/input/log/sample6_retrieve_log.doc +1 -0
  57. data/spec/input/log/sample7_dag_update.doc +1 -0
  58. data/spec/input/log/sample7_retrieve_log.doc +1 -0
  59. data/spec/input/log/sample8_dag_update.doc +1 -0
  60. data/spec/input/log/sample_addhash.doc +1 -0
  61. data/spec/input/log/sample_dag_update.doc +1 -0
  62. data/spec/input/log/sample_match_log.doc +1 -0
  63. data/spec/input/log/sample_op1_addhash.doc +1 -0
  64. data/spec/input/log/sample_retrieve_log.doc +1 -0
  65. data/spec/input/main/sample0_read.doc +1 -0
  66. data/spec/output/basic/arrays.json +1 -0
  67. data/spec/output/basic/french.json +1 -0
  68. data/spec/output/basic/sample2_get_location.doc +1 -0
  69. data/spec/output/basic/sample2_retrieve_document.doc +1 -0
  70. data/spec/output/basic/sample3_retrieve_document.doc +1 -0
  71. data/spec/output/basic/sample4_retrieve_document.doc +1 -0
  72. data/spec/output/basic/sample5_retrieve_document.doc +1 -0
  73. data/spec/output/basic/sample_dec.doc +1 -0
  74. data/spec/output/basic/sample_enc.doc +1 -0
  75. data/spec/output/basic/sample_get_location.doc +1 -0
  76. data/spec/output/basic/sample_hash.doc +1 -0
  77. data/spec/output/basic/sample_invalid2_readkey.doc +1 -0
  78. data/spec/output/basic/sample_invalid2_verify.doc +1 -0
  79. data/spec/output/basic/sample_invalid3_readkey.doc +1 -0
  80. data/spec/output/basic/sample_invalid3_verify.doc +1 -0
  81. data/spec/output/basic/sample_invalid_privkey.doc +1 -0
  82. data/spec/output/basic/sample_invalid_readkey.doc +1 -0
  83. data/spec/output/basic/sample_invalid_sign.doc +1 -0
  84. data/spec/output/basic/sample_invalid_verify.doc +1 -0
  85. data/spec/output/basic/sample_key.doc +1 -0
  86. data/spec/output/basic/sample_readkey.doc +1 -0
  87. data/spec/output/basic/sample_retrieve_document.doc +1 -0
  88. data/spec/output/basic/sample_sign.doc +1 -0
  89. data/spec/output/basic/sample_valid_privkey.doc +1 -0
  90. data/spec/output/basic/sample_verify.doc +1 -0
  91. data/spec/output/basic/structures.json +1 -0
  92. data/spec/output/basic/unicode.json +1 -0
  93. data/spec/output/basic/values.json +1 -0
  94. data/spec/output/basic/wierd.json +1 -0
  95. data/spec/output/log/sample0_dag2array.doc +1 -0
  96. data/spec/output/log/sample0_dag_did.doc +1 -0
  97. data/spec/output/log/sample1_dag_did.doc +1 -0
  98. data/spec/output/log/sample1_dag_update.doc +1 -0
  99. data/spec/output/log/sample2_dag_did.doc +1 -0
  100. data/spec/output/log/sample2_dag_update.doc +1 -0
  101. data/spec/output/log/sample2_retrieve_log.doc +1 -0
  102. data/spec/output/log/sample3_dag_did.doc +1 -0
  103. data/spec/output/log/sample3_dag_update.doc +1 -0
  104. data/spec/output/log/sample3_retrieve_log.doc +1 -0
  105. data/spec/output/log/sample4_dag_did.doc +1 -0
  106. data/spec/output/log/sample4_dag_update.doc +1 -0
  107. data/spec/output/log/sample4_retrieve_log.doc +1 -0
  108. data/spec/output/log/sample5_dag_update.doc +1 -0
  109. data/spec/output/log/sample5_retrieve_log.doc +1 -0
  110. data/spec/output/log/sample6_dag_update.doc +1 -0
  111. data/spec/output/log/sample6_retrieve_log.doc +1 -0
  112. data/spec/output/log/sample7_dag_update.doc +1 -0
  113. data/spec/output/log/sample7_retrieve_log.doc +1 -0
  114. data/spec/output/log/sample8_dag_update.doc +1 -0
  115. data/spec/output/log/sample_addhash.doc +1 -0
  116. data/spec/output/log/sample_dag_update.doc +1 -0
  117. data/spec/output/log/sample_match_log.doc +1 -0
  118. data/spec/output/log/sample_op1_addhash.doc +1 -0
  119. data/spec/output/log/sample_retrieve_log.doc +1 -0
  120. data/spec/output/main/sample0_read.doc +1 -0
  121. data/spec/oydid_spec.rb +170 -0
  122. data/spec/spec_helper.rb +31 -0
  123. metadata +420 -0
data/lib/oydid.rb ADDED
@@ -0,0 +1,738 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'dag'
5
+ require 'jwt'
6
+ require 'rbnacl'
7
+ require 'ed25519'
8
+ require 'httparty'
9
+ require 'multibases'
10
+ require 'multihashes'
11
+ require 'multicodecs'
12
+ require 'json/canonicalization'
13
+ require 'ppldid/basic'
14
+ require 'ppldid/log'
15
+ require 'ppldid/didcomm'
16
+
17
+ class Oydid
18
+
19
+ LOCATION_PREFIX = "@"
20
+ DEFAULT_LOCATION = "https://ppldid.ownyourdata.eu"
21
+
22
+
23
+ # expected DID format: did:ppld:123
24
+ def self.read(did, options)
25
+ # setup
26
+ currentDID = {
27
+ "did": did,
28
+ "doc": "",
29
+ "log": [],
30
+ "doc_log_id": nil,
31
+ "termination_log_id": nil,
32
+ "error": 0,
33
+ "message": "",
34
+ "verification": ""
35
+ }.transform_keys(&:to_s)
36
+ did_hash = did.delete_prefix("did:ppld:")
37
+ did10 = did_hash[0,10]
38
+
39
+ # get did location
40
+ did_location = ""
41
+ if !options[:doc_location].nil?
42
+ did_location = options[:doc_location]
43
+ end
44
+ if did_location.to_s == ""
45
+ if !options[:location].nil?
46
+ did_location = options[:location]
47
+ end
48
+ end
49
+ if did_location.to_s == ""
50
+ if did.include?(LOCATION_PREFIX)
51
+ tmp = did.split(LOCATION_PREFIX)
52
+ did = tmp[0]
53
+ did_location = tmp[1]
54
+ end
55
+ end
56
+ if did_location == ""
57
+ did_location = DEFAULT_LOCATION
58
+ end
59
+
60
+ # retrieve DID document
61
+ did_document = retrieve_document(did, did10 + ".doc", did_location, options)
62
+ if did_document.first.nil?
63
+ return [nil, did_document.last]
64
+ end
65
+ did_document = did_document.first
66
+ currentDID["doc"] = did_document
67
+ if options[:trace]
68
+ puts " .. DID document retrieved"
69
+ end
70
+
71
+ # get log location
72
+ log_hash = did_document["log"]
73
+ log_location = ""
74
+ if !options[:log_location].nil?
75
+ log_location = options[:log_location]
76
+ end
77
+ if log_location.to_s == ""
78
+ if !options[:location].nil?
79
+ log_location = options[:location]
80
+ end
81
+ end
82
+ if log_location.to_s == ""
83
+ if log_hash.include?(LOCATION_PREFIX)
84
+ hash_split = log_hash.split(LOCATION_PREFIX)
85
+ log_hash = hash_split[0]
86
+ log_location = hash_split[1]
87
+ end
88
+ end
89
+ if log_location == ""
90
+ log_location = DEFAULT_LOCATION
91
+ end
92
+
93
+ # retrieve and traverse log to get current DID state
94
+ log_array, msg = retrieve_log(log_hash, did10 + ".log", log_location, options)
95
+ if log_array.nil?
96
+ return [nil, msg]
97
+ else
98
+ if options[:trace]
99
+ puts " .. Log retrieved"
100
+ end
101
+ dag, create_index, terminate_index, msg = dag_did(log_array, options)
102
+ if dag.nil?
103
+ return [nil, msg]
104
+ end
105
+ if options[:trace]
106
+ puts " .. DAG with " + dag.vertices.length.to_s + " vertices and " + dag.edges.length.to_s + " edges, CREATE index: " + create_index.to_s
107
+ end
108
+ ordered_log_array = dag2array(dag, log_array, create_index, [], options)
109
+ ordered_log_array << log_array[terminate_index]
110
+ currentDID["log"] = ordered_log_array
111
+ if options[:trace]
112
+ if options[:silent].nil? || !options[:silent]
113
+ puts " vertex " + terminate_index.to_s + " at " + log_array[terminate_index]["ts"].to_s + " op: " + log_array[terminate_index]["op"].to_s + " doc: " + log_array[terminate_index]["doc"].to_s
114
+ end
115
+ end
116
+ currentDID["log"] = ordered_log_array
117
+ if options[:trace]
118
+ if options[:silent].nil? || !options[:silent]
119
+ dag.edges.each do |e|
120
+ puts " edge " + e.origin[:id].to_s + " <- " + e.destination[:id].to_s
121
+ end
122
+ end
123
+ end
124
+ currentDID = dag_update(currentDID, options)
125
+ if options[:log_complete]
126
+ currentDID["log"] = log_array
127
+ end
128
+
129
+ return [currentDID, ""]
130
+ end
131
+
132
+ end
133
+
134
+ def self.create(content, options)
135
+ return write(content, nil, "create", options)
136
+ end
137
+
138
+ def self.update(content, did, options)
139
+ return write(content, did, "update", options)
140
+ end
141
+
142
+ def self.simulate_did(content, did, mode, options)
143
+ user_did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, msg = Oydid.generate_base(content, did, mode, options)
144
+ return [user_did, msg]
145
+ end
146
+
147
+ def self.generate_base(content, did, mode, options)
148
+ # input validation
149
+ did_doc = JSON.parse(content.to_json) rescue nil
150
+ if did_doc.nil?
151
+ return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid payload"]
152
+ end
153
+ did_old = nil
154
+ log_old = nil
155
+ prev_hash = []
156
+ revoc_log = nil
157
+ doc_location = options[:location]
158
+ if options[:ts].nil?
159
+ ts = Time.now.to_i
160
+ else
161
+ ts = options[:ts]
162
+ end
163
+
164
+ if mode == "create" || mode == "clone"
165
+ operation_mode = 2 # CREATE
166
+ if options[:doc_key].nil?
167
+ if options[:doc_enc].nil?
168
+ privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv')
169
+ else
170
+ privateKey, msg = decode_private_key(options[:doc_enc].to_s)
171
+ end
172
+ else
173
+ privateKey, msg = read_private_key(options[:doc_key].to_s)
174
+ if privateKey.nil?
175
+ return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "private document key not found"]
176
+ end
177
+ end
178
+ if options[:rev_key].nil?
179
+ if options[:rev_enc].nil?
180
+ revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv')
181
+ else
182
+ revocationKey, msg = decode_private_key(options[:rev_enc].to_s)
183
+ end
184
+ else
185
+ revocationKey, msg = read_private_key(options[:rev_key].to_s)
186
+ if revocationKey.nil?
187
+ return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "private revocation key not found"]
188
+ end
189
+ end
190
+ else # mode == "update" => read information
191
+ did_info, msg = read(did, options)
192
+ if did_info.nil?
193
+ return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "cannot resolve DID (on updating DID)"]
194
+ end
195
+ if did_info["error"] != 0
196
+ return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, did_info["message"].to_s]
197
+ end
198
+
199
+ did = did_info["did"]
200
+ did_hash = did.delete_prefix("did:ppld:")
201
+ did10 = did_hash[0,10]
202
+ if doc_location.to_s == ""
203
+ if did_hash.include?(LOCATION_PREFIX)
204
+ hash_split = did_hash.split(LOCATION_PREFIX)
205
+ did_hash = hash_split[0]
206
+ doc_location = hash_split[1]
207
+ end
208
+ end
209
+ operation_mode = 3 # UPDATE
210
+
211
+ # collect relevant information from previous did
212
+ did_old = did.dup
213
+ did10_old = did10.dup
214
+ log_old = did_info["log"]
215
+ if options[:old_doc_key].nil?
216
+ if options[:old_doc_enc].nil?
217
+ if options[:old_doc_pwd].nil?
218
+ privateKey_old = read_private_storage(did10_old + "_private_key.b58")
219
+ else
220
+ privateKey_old, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv')
221
+ end
222
+ else
223
+ privateKey_old, msg = decode_private_key(options[:old_doc_enc].to_s)
224
+ end
225
+ else
226
+ privateKey_old, msg = read_private_key(options[:old_doc_key].to_s)
227
+ end
228
+ if privateKey_old.nil?
229
+ return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old private document key"]
230
+ end
231
+ if options[:old_rev_key].nil?
232
+ if options[:old_rev_enc].nil?
233
+ if options[:old_rev_pwd].nil?
234
+ revocationKey_old = read_private_storage(did10_old + "_revocation_key.b58")
235
+ else
236
+ revocationKey_old, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv')
237
+ end
238
+ else
239
+ revocationKey_old, msg = decode_private_key(options[:old_rev_enc].to_s)
240
+ end
241
+ else
242
+ revocationKey_old, msg = read_private_key(options[:old_rev_key].to_s)
243
+ end
244
+ if revocationKey_old.nil?
245
+ return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old private revocation key"]
246
+ end
247
+
248
+ # key management
249
+ if options[:doc_key].nil?
250
+ if options[:doc_enc].nil?
251
+ privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv')
252
+ else
253
+ privateKey, msg = decode_private_key(options[:doc_enc].to_s)
254
+ end
255
+ else
256
+ privateKey, msg = read_private_key(options[:doc_key].to_s)
257
+ end
258
+ # if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil?
259
+ # revocationLog = read_private_storage(did10 + "_revocation.json")
260
+ # if revocationLog.nil?
261
+ # return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old revocation log"]
262
+ # end
263
+ # else
264
+ if options[:rev_key].nil?
265
+ if options[:rev_enc].nil?
266
+ if options[:rev_pwd].nil?
267
+ revocationKey, msg = generate_private_key("", 'ed25519-priv')
268
+ else
269
+ revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv')
270
+ end
271
+ else
272
+ revocationKey, msg = decode_private_key(options[:rev_enc].to_s)
273
+ end
274
+ else
275
+ revocationKey, msg = read_private_key(options[:rev_key].to_s)
276
+ end
277
+
278
+ # re-build revocation document
279
+ did_old_doc = did_info["doc"]["doc"]
280
+ ts_old = did_info["log"].last["ts"]
281
+ publicKey_old = public_key(privateKey_old).first
282
+ pubRevoKey_old = public_key(revocationKey_old).first
283
+ did_key_old = publicKey_old + ":" + pubRevoKey_old
284
+ subDid = {"doc": did_old_doc, "key": did_key_old}.to_json
285
+ subDidHash = hash(canonical(subDid))
286
+ signedSubDidHash = sign(subDidHash, revocationKey_old).first
287
+ revocationLog = {
288
+ "ts": ts_old,
289
+ "op": 1, # REVOKE
290
+ "doc": subDidHash,
291
+ "sig": signedSubDidHash }.transform_keys(&:to_s).to_json
292
+ # end
293
+ revoc_log = JSON.parse(revocationLog)
294
+ revoc_log["previous"] = [
295
+ hash(canonical(log_old[did_info["doc_log_id"].to_i])),
296
+ hash(canonical(log_old[did_info["termination_log_id"].to_i]))
297
+ ]
298
+ prev_hash = [hash(canonical(revoc_log))]
299
+ end
300
+
301
+ publicKey = public_key(privateKey).first
302
+ pubRevoKey = public_key(revocationKey).first
303
+ did_key = publicKey + ":" + pubRevoKey
304
+
305
+ # build new revocation document
306
+ subDid = {"doc": did_doc, "key": did_key}.to_json
307
+ subDidHash = hash(canonical(subDid))
308
+ signedSubDidHash = sign(subDidHash, revocationKey).first
309
+ r1 = { "ts": ts,
310
+ "op": 1, # REVOKE
311
+ "doc": subDidHash,
312
+ "sig": signedSubDidHash }.transform_keys(&:to_s)
313
+
314
+ # build termination log entry
315
+ l2_doc = hash(canonical(r1))
316
+ if !doc_location.nil?
317
+ l2_doc += LOCATION_PREFIX + doc_location.to_s
318
+ end
319
+ l2 = { "ts": ts,
320
+ "op": 0, # TERMINATE
321
+ "doc": l2_doc,
322
+ "sig": sign(l2_doc, privateKey).first,
323
+ "previous": [] }.transform_keys(&:to_s)
324
+
325
+ # build actual DID document
326
+ log_str = hash(canonical(l2))
327
+ if !doc_location.nil?
328
+ log_str += LOCATION_PREFIX + doc_location.to_s
329
+ end
330
+ didDocument = { "doc": did_doc,
331
+ "key": did_key,
332
+ "log": log_str }.transform_keys(&:to_s)
333
+
334
+ # create DID
335
+ l1_doc = hash(canonical(didDocument))
336
+ if !doc_location.nil?
337
+ l1_doc += LOCATION_PREFIX + doc_location.to_s
338
+ end
339
+ did = "did:ppld:" + l1_doc
340
+ did10 = l1_doc[0,10]
341
+
342
+ if mode == "clone"
343
+ # create log entry for source DID
344
+ new_log = {
345
+ "ts": ts,
346
+ "op": 4, # CLONE
347
+ "doc": l1_doc,
348
+ "sig": sign(l1_doc, privateKey).first,
349
+ "previous": [options[:previous_clone].to_s]
350
+ }
351
+ retVal = HTTParty.post(options[:source_location] + "/log/" + options[:source_did],
352
+ headers: { 'Content-Type' => 'application/json' },
353
+ body: {"log": new_log}.to_json )
354
+ prev_hash = [hash(canonical(new_log))]
355
+ end
356
+
357
+ # build creation log entry
358
+ if operation_mode == 3 # UPDATE
359
+ l1 = { "ts": ts,
360
+ "op": operation_mode, # UPDATE
361
+ "doc": l1_doc,
362
+ "sig": sign(l1_doc, privateKey_old).first,
363
+ "previous": prev_hash }.transform_keys(&:to_s)
364
+ else
365
+ l1 = { "ts": ts,
366
+ "op": operation_mode, # CREATE
367
+ "doc": l1_doc,
368
+ "sig": sign(l1_doc, privateKey).first,
369
+ "previous": prev_hash }.transform_keys(&:to_s)
370
+ end
371
+
372
+ return [did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, ""]
373
+ end
374
+
375
+ def self.publish(did, didDocument, logs, options)
376
+ did_hash = did.delete_prefix("did:ppld:")
377
+ did10 = did_hash[0,10]
378
+
379
+ doc_location = options[:doc_location]
380
+ if doc_location.to_s == ""
381
+ if did_hash.include?(LOCATION_PREFIX)
382
+ hash_split = did_hash.split(LOCATION_PREFIX)
383
+ did_hash = hash_split[0]
384
+ doc_location = hash_split[1]
385
+ else
386
+ doc_location = DEFAULT_LOCATION
387
+ end
388
+ end
389
+
390
+ # wirte data based on location
391
+ case doc_location.to_s
392
+ when /^http/
393
+ # build object to post
394
+ did_data = {
395
+ "did": did,
396
+ "did-document": didDocument,
397
+ "logs": logs
398
+ }
399
+ oydid_url = doc_location.to_s + "/doc"
400
+ retVal = HTTParty.post(oydid_url,
401
+ headers: { 'Content-Type' => 'application/json' },
402
+ body: did_data.to_json )
403
+ if retVal.code != 200
404
+ err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc"
405
+ return [false, err_msg]
406
+ end
407
+ else
408
+ # write files to disk
409
+ write_private_storage(logs.to_json, did10 + ".log")
410
+ write_private_storage(didDocument.to_json, did10 + ".doc")
411
+ write_private_storage(did, did10 + ".did")
412
+ end
413
+ return [true, ""]
414
+
415
+ end
416
+
417
+ def self.write(content, did, mode, options)
418
+ did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, msg = generate_base(content, did, mode, options)
419
+ if msg != ""
420
+ return [nil, msg]
421
+ end
422
+
423
+ did_hash = did.delete_prefix("did:ppld:")
424
+ did10 = did_hash[0,10]
425
+ did_old_hash = did_old.delete_prefix("did:ppld:") rescue nil
426
+ did10_old = did_old_hash[0,10] rescue nil
427
+
428
+ doc_location = options[:doc_location]
429
+ if doc_location.to_s == ""
430
+ if did_hash.include?(LOCATION_PREFIX)
431
+ hash_split = did_hash.split(LOCATION_PREFIX)
432
+ did_hash = hash_split[0]
433
+ doc_location = hash_split[1]
434
+ else
435
+ doc_location = DEFAULT_LOCATION
436
+ end
437
+ end
438
+
439
+ case doc_location.to_s
440
+ when /^http/
441
+ logs = [revoc_log, l1, l2].flatten.compact
442
+ else
443
+ logs = [log_old, revoc_log, l1, l2].flatten.compact
444
+ if !did_old.nil?
445
+ write_private_storage([log_old, revoc_log, l1, l2].flatten.compact.to_json, did10_old + ".log")
446
+ end
447
+ end
448
+ success, msg = publish(did, didDocument, logs, options)
449
+
450
+ if success
451
+ w3c_input = {
452
+ "did" => did,
453
+ "doc" => didDocument
454
+ }
455
+ retVal = {
456
+ "did" => did,
457
+ "doc" => didDocument,
458
+ "doc_w3c" => w3c(w3c_input, options),
459
+ "log" => logs
460
+ }
461
+ if options[:return_secrets]
462
+ retVal["private_key"] = privateKey
463
+ retVal["revocation_key"] = revocationKey
464
+ retVal["revocation_log"] = r1
465
+ else
466
+ write_private_storage(privateKey, did10 + "_private_key.b58")
467
+ write_private_storage(revocationKey, did10 + "_revocation_key.b58")
468
+ write_private_storage(r1.to_json, did10 + "_revocation.json")
469
+ end
470
+
471
+ return [retVal, ""]
472
+ else
473
+ return [nil, msg]
474
+ end
475
+ end
476
+
477
+ def self.revoke_base(did, options)
478
+ did_orig = did.dup
479
+ doc_location = options[:doc_location]
480
+ if options[:ts].nil?
481
+ ts = Time.now.to_i
482
+ else
483
+ ts = options[:ts]
484
+ end
485
+ did_info, msg = read(did, options)
486
+ if did_info.nil?
487
+ return [nil, "cannot resolve DID (on revoking DID)"]
488
+ end
489
+ if did_info["error"] != 0
490
+ return [nil, did_info["message"].to_s]
491
+ end
492
+
493
+ did = did_info["did"]
494
+ did_hash = did.delete_prefix("did:ppld:")
495
+ did10 = did_hash[0,10]
496
+ if doc_location.to_s == ""
497
+ if did_hash.include?(LOCATION_PREFIX)
498
+ hash_split = did_hash.split(LOCATION_PREFIX)
499
+ did_hash = hash_split[0]
500
+ doc_location = hash_split[1]
501
+ end
502
+ end
503
+
504
+ # collect relevant information from previous did
505
+ did_old = did.dup
506
+ did10_old = did10.dup
507
+ log_old = did_info["log"]
508
+
509
+ if options[:old_doc_key].nil?
510
+ if options[:old_doc_enc].nil?
511
+ if options[:old_doc_pwd].nil?
512
+ privateKey_old = read_private_storage(did10_old + "_private_key.b58")
513
+ else
514
+ privateKey_old, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv')
515
+ end
516
+ else
517
+ privateKey_old, msg = decode_private_key(options[:old_doc_enc].to_s)
518
+ end
519
+ else
520
+ privateKey_old, msg = read_private_key(options[:old_doc_key].to_s)
521
+ end
522
+ if privateKey_old.nil?
523
+ return [nil, "invalid or missing old private document key"]
524
+ end
525
+ if options[:old_rev_key].nil?
526
+ if options[:old_rev_enc].nil?
527
+ if options[:old_rev_pwd].nil?
528
+ revocationKey_old = read_private_storage(did10_old + "_revocation_key.b58")
529
+ else
530
+ revocationKey_old, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv')
531
+ end
532
+ else
533
+ revocationKey_old, msg = decode_private_key(options[:old_rev_enc].to_s)
534
+ end
535
+ else
536
+ revocationKey_old, msg = read_private_key(options[:old_rev_key].to_s)
537
+ end
538
+ if revocationKey_old.nil?
539
+ return [nil, "invalid or missing old private revocation key"]
540
+ end
541
+
542
+ if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil?
543
+ revocationKey, msg = read_private_key(did10 + "_revocation_key.b58")
544
+ revocationLog = read_private_storage(did10 + "_revocation.json")
545
+ else
546
+ if options[:rev_pwd].nil?
547
+ if options[:rev_enc].nil?
548
+ revocationKey, msg = read_private_key(options[:rev_key].to_s)
549
+ else
550
+ revocationKey, msg = decode_private_key(options[:rev_enc].to_s)
551
+ end
552
+ else
553
+ revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv')
554
+ end
555
+ # re-build revocation document
556
+ did_old_doc = did_info["doc"]["doc"]
557
+ ts_old = did_info["log"].last["ts"]
558
+ publicKey_old = public_key(privateKey_old).first
559
+ pubRevoKey_old = public_key(revocationKey_old).first
560
+ did_key_old = publicKey_old + ":" + pubRevoKey_old
561
+ subDid = {"doc": did_old_doc, "key": did_key_old}.to_json
562
+ subDidHash = hash(canonical(subDid))
563
+ signedSubDidHash = sign(subDidHash, revocationKey_old).first
564
+ revocationLog = {
565
+ "ts": ts_old,
566
+ "op": 1, # REVOKE
567
+ "doc": subDidHash,
568
+ "sig": signedSubDidHash }.transform_keys(&:to_s).to_json
569
+ end
570
+
571
+ if revocationLog.nil?
572
+ return [nil, "private revocation key not found"]
573
+ end
574
+
575
+ revoc_log = JSON.parse(revocationLog)
576
+ revoc_log["previous"] = [
577
+ hash(canonical(log_old[did_info["doc_log_id"].to_i])),
578
+ hash(canonical(log_old[did_info["termination_log_id"].to_i]))
579
+ ]
580
+ return [revoc_log, ""]
581
+ end
582
+
583
+ def self.revoke_publish(did, revoc_log, options)
584
+ did_hash = did.delete_prefix("did:ppld:")
585
+ did10 = did_hash[0,10]
586
+ doc_location = options[:doc_location]
587
+ if did_hash.include?(LOCATION_PREFIX)
588
+ hash_split = did_hash.split(LOCATION_PREFIX)
589
+ did_hash = hash_split[0]
590
+ doc_location = hash_split[1]
591
+ end
592
+ if doc_location.to_s == ""
593
+ doc_location = DEFAULT_LOCATION
594
+ end
595
+
596
+ # publish revocation log based on location
597
+ case doc_location.to_s
598
+ when /^http/
599
+ retVal = HTTParty.post(doc_location.to_s + "/log/" + did_hash.to_s,
600
+ headers: { 'Content-Type' => 'application/json' },
601
+ body: {"log": revoc_log}.to_json )
602
+ if retVal.code != 200
603
+ msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/log/" + did_hash.to_s
604
+ return [nil, msg]
605
+ end
606
+ else
607
+ File.write(did10 + ".log", revoc_log.to_json)
608
+ if !did_old.nil?
609
+ File.write(did10_old + ".log", revoc_log.to_json)
610
+ end
611
+ end
612
+
613
+ return [did, ""]
614
+ end
615
+
616
+ def self.revoke(did, options)
617
+ revoc_log, msg = revoke_base(did, options)
618
+ if revoc_log.nil?
619
+ return [nil, msg]
620
+ end
621
+ success, msg = revoke_publish(did, revoc_log, options)
622
+ end
623
+
624
+ def self.clone(did, options)
625
+ # check if locations differ
626
+ target_location = options[:doc_location]
627
+ if target_location.to_s == ""
628
+ target_location = DEFAULT_LOCATION
629
+ end
630
+ if did.include?(LOCATION_PREFIX)
631
+ hash_split = did.split(LOCATION_PREFIX)
632
+ did = hash_split[0]
633
+ source_location = hash_split[1]
634
+ end
635
+ if source_location.to_s == ""
636
+ source_location = DEFAULT_LOCATION
637
+ end
638
+ if target_location == source_location
639
+ return [nil, "cannot clone to same location (" + target_location.to_s + ")"]
640
+ end
641
+
642
+ # get original did info
643
+ options[:doc_location] = source_location
644
+ options[:log_location] = source_location
645
+ source_did, msg = read(did, options)
646
+ if source_did.nil?
647
+ return [nil, "cannot resolve DID (on cloning DID)"]
648
+ end
649
+ if source_did["error"] != 0
650
+ return [nil, source_did["message"].to_s]
651
+ end
652
+ if source_did["doc_log_id"].nil?
653
+ return [nil, "cannot parse DID log"]
654
+ end
655
+ source_log = source_did["log"].first(source_did["doc_log_id"] + 1).last.to_json
656
+
657
+ # write did to new location
658
+ options[:doc_location] = target_location
659
+ options[:log_location] = target_location
660
+ options[:previous_clone] = hash(canonical(source_log)) + LOCATION_PREFIX + source_location
661
+ options[:source_location] = source_location
662
+ options[:source_did] = source_did["did"]
663
+ retVal, msg = write(source_did["doc"]["doc"], nil, "clone", options)
664
+ return [retVal, msg]
665
+ end
666
+
667
+ def self.w3c(did_info, options)
668
+ did = did_info["did"]
669
+ if !did.start_with?("did:ppld:")
670
+ did = "did:ppld:" + did
671
+ end
672
+
673
+ didDoc = did_info.transform_keys(&:to_s)["doc"]
674
+ pubDocKey = didDoc["key"].split(":")[0] rescue ""
675
+ pubRevKey = didDoc["key"].split(":")[1] rescue ""
676
+
677
+ wd = {}
678
+ wd["@context"] = "https://www.w3.org/ns/did/v1"
679
+ wd["id"] = did
680
+ wd["verificationMethod"] = [{
681
+ "id": did + "#key-doc",
682
+ "type": "Ed25519VerificationKey2020",
683
+ "controller": did,
684
+ "publicKeyMultibase": pubDocKey
685
+ },{
686
+ "id": did + "#key-rev",
687
+ "type": "Ed25519VerificationKey2020",
688
+ "controller": did,
689
+ "publicKeyMultibase": pubRevKey
690
+ }]
691
+
692
+ if didDoc["@context"].to_s == "https://www.w3.org/ns/did/v1"
693
+ didDoc.delete("@context")
694
+ end
695
+ if didDoc["doc"].to_s != ""
696
+ didDoc = didDoc["doc"]
697
+ end
698
+ newDidDoc = []
699
+ if didDoc.is_a?(Hash)
700
+ if didDoc["authentication"].to_s != ""
701
+ wd["authentication"] = didDoc["authentication"]
702
+ didDoc.delete("authentication")
703
+ end
704
+ if didDoc["service"].to_s != ""
705
+ if didDoc["service"].is_a?(Array)
706
+ newDidDoc = didDoc.dup
707
+ newDidDoc.delete("service")
708
+ if newDidDoc == {}
709
+ newDidDoc = []
710
+ else
711
+ if !newDidDoc.is_a?(Array)
712
+ newDidDoc=[newDidDoc]
713
+ end
714
+ end
715
+ newDidDoc << didDoc["service"]
716
+ newDidDoc = newDidDoc.flatten
717
+ else
718
+ newDidDoc = didDoc["service"]
719
+ end
720
+ else
721
+ newDidDoc = didDoc
722
+ end
723
+ else
724
+ newDidDoc = didDoc
725
+ end
726
+ wd["service"] = newDidDoc
727
+ return wd
728
+ end
729
+
730
+ def self.fromW3C(didDocument, options)
731
+ didDocument = didDocument.transform_keys(&:to_s)
732
+ if didDocument["@context"].to_s == "https://www.w3.org/ns/did/v1"
733
+ didDocument.delete("@context")
734
+ end
735
+ didDocument
736
+ end
737
+
738
+ end