oydid 0.2.0

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