oydid 0.5.4 → 0.5.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 171ab373123506d42a8dd20b80df654ebd3cf35ac1d53e65f36580b4a3d92374
4
- data.tar.gz: 2bab378e7f1d284f7e730f32acb52964f7c84456ae5a41a786429d0f6ff11134
3
+ metadata.gz: e6e4098a11373a9b648447adbc7f6ec7b89d0efced1bb920080797dfdc5d17de
4
+ data.tar.gz: bf33e0ddd9abd26a04d92fa945db9183f87000e246f154aa509c55e9b5f4aefd
5
5
  SHA512:
6
- metadata.gz: e3d84952f40b4b7680f1c49cb351516b25b901f696f4db73222a3a5f82a645ee23da62881f24237e29cac195da2857f4392ab289ba78d29f83a14332d970ea91
7
- data.tar.gz: 555e144d9581d897b91eadd480267c9e76bfed1145077cf524537d13cc571be3efca39512aefdb7ce798390de22fc87ea922ac6e53db3e67dfba8dfa65cedd08
6
+ metadata.gz: 6175bb5b1d28443b26573754dd92f97094a7c139d032252c541a73dbd5019028466847b9602b380cd1ca814eeb3e7843576f7a86a77808a5268af62fe89dfba7
7
+ data.tar.gz: a719e0d4c5530340be4ad4a284ffadd46924db95a81c0212fb61e38243cac6a133bf8568ed6407a54d026502048bb06394f5e737f12e0ad588b65c43411bb046
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.4
1
+ 0.5.6
data/lib/oydid/basic.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # frozen_string_literal: true
3
3
 
4
+ class Hash
5
+ def except(*keys)
6
+ self.reject { |key, _| keys.include?(key) }
7
+ end
8
+ end
9
+
4
10
  class Oydid
5
11
 
6
12
  # basic functions ---------------------------
7
- # %w[multibases multihashes rbnacl json].each { |f| require f }
13
+ # %w[multibases multihashes rbnacl json multicodecs].each { |f| require f }
8
14
  def self.multi_encode(message, options)
9
15
  method = options[:encode] || DEFAULT_ENCODING rescue DEFAULT_ENCODING
10
16
  case method
@@ -54,7 +60,11 @@ class Oydid
54
60
  end
55
61
 
56
62
  def self.get_digest(message)
57
- retVal = Multihashes.decode Oydid.multi_decode(message).first
63
+ decoded_message, error = Oydid.multi_decode(message)
64
+ if decoded_message.nil?
65
+ return [nil, error]
66
+ end
67
+ retVal = Multihashes.decode decoded_message
58
68
  if retVal[:hash_function].to_s != ""
59
69
  return [retVal[:hash_function].to_s, ""]
60
70
  end
@@ -94,7 +104,7 @@ class Oydid
94
104
  end
95
105
 
96
106
  # key management ----------------------------
97
- def self.generate_private_key(input, method = "ed25519-priv", options)
107
+ def self.generate_private_key(input, method = "ed25519-priv", options = {})
98
108
  begin
99
109
  omc = Multicodecs[method].code
100
110
  rescue
@@ -104,15 +114,18 @@ class Oydid
104
114
  case Multicodecs[method].name
105
115
  when 'ed25519-priv'
106
116
  if input != ""
107
- raw_key = Ed25519::SigningKey.new(RbNaCl::Hash.sha256(input)).to_bytes
117
+ raw_key = Ed25519::SigningKey.new(RbNaCl::Hash.sha256(input))
108
118
  else
109
- raw_key = Ed25519::SigningKey.generate.to_bytes
119
+ raw_key = Ed25519::SigningKey.generate
110
120
  end
121
+ raw_key = raw_key.to_bytes
122
+ length = raw_key.bytesize
111
123
  else
112
124
  return [nil, "unsupported key codec"]
113
125
  end
114
- length = raw_key.bytesize
126
+
115
127
  encoded = multi_encode([omc, length, raw_key].pack("SCa#{length}"), options)
128
+ # encoded = multi_encode(raw_key.to_bytes, options)
116
129
  if encoded.first.nil?
117
130
  return [nil, encoded.last]
118
131
  else
@@ -120,7 +133,7 @@ class Oydid
120
133
  end
121
134
  end
122
135
 
123
- def self.public_key(private_key, options, method = "ed25519-pub")
136
+ def self.public_key(private_key, options = {}, method = "ed25519-pub")
124
137
  code, length, digest = multi_decode(private_key).first.unpack('SCa*')
125
138
  case Multicodecs[code].name
126
139
  when 'ed25519-priv'
@@ -132,6 +145,7 @@ class Oydid
132
145
  else
133
146
  return [nil, "unsupported key codec"]
134
147
  end
148
+ # encoded = multi_encode(public_key.to_bytes, options)
135
149
  length = public_key.to_bytes.bytesize
136
150
  encoded = multi_encode([Multicodecs[method].code, length, public_key].pack("CCa#{length}"), options)
137
151
  if encoded.first.nil?
@@ -144,6 +158,29 @@ class Oydid
144
158
  end
145
159
  end
146
160
 
161
+ def self.getPrivateKey(enc, pwd, dsk, dfl, options)
162
+ if enc.to_s == "" # usually read from options[:doc_enc]
163
+ if pwd.to_s == "" # usually read from options[:doc_pwd]
164
+ if dsk.to_s == "" # usually read from options[:doc_key]
165
+ if dfl.to_s == "" # default file name for key
166
+ return [nil, "no reference"]
167
+ else
168
+ privateKey, msg = read_private_key(dfl.to_s, options)
169
+ end
170
+ else
171
+ privateKey, msg = read_private_key(dsk.to_s, options)
172
+ end
173
+ else
174
+ privateKey, msg = generate_private_key(pwd, 'ed25519-priv', options)
175
+ end
176
+ else
177
+ privateKey, msg = decode_private_key(enc.to_s, options)
178
+ end
179
+ return [privateKey, msg]
180
+ end
181
+
182
+ # if the identifier is already the public key there is no validation if it is a valid key
183
+ # (this is a privacy-preserving feature)
147
184
  def self.getPubKeyFromDID(did)
148
185
  identifier = did.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first rescue did
149
186
  identifier = identifier.delete_prefix("did:oyd:")
@@ -166,6 +203,52 @@ class Oydid
166
203
  end
167
204
  end
168
205
 
206
+ # available key_types
207
+ # * doc - document key
208
+ # * rev - revocation key
209
+ def self.getDelegatedPubKeysFromDID(did, key_type = "doc")
210
+ # retrieve DID
211
+ did_document, msg = read(did, {})
212
+ keys, msg = getDelegatedPubKeysFromFullDidDocument(did_document, key_type)
213
+ if keys.nil?
214
+ return [nil, msg]
215
+ else
216
+ return [keys, ""]
217
+ end
218
+ end
219
+
220
+ def self.getDelegatedPubKeysFromFullDidDocument(did_document, key_type = "doc")
221
+ # get current public key
222
+ case key_type
223
+ when "doc"
224
+ keys = [did_document["doc"]["key"].split(":").first] rescue nil
225
+ when "rev"
226
+ keys = [did_document["doc"]["key"].split(":").last] rescue nil
227
+ else
228
+ return [nil, "invalid key type: " + key_type]
229
+ end
230
+ if keys.nil?
231
+ return [nil, "cannot retrieve current key"]
232
+ end
233
+
234
+ # travers through log and get active delegation public keys
235
+ log = did_document["log"]
236
+ log.each do |item|
237
+ if item["op"] == 5 # DELEGATE
238
+ # !!!OPEN: check if log entry is confirmed / referenced in a termination entry
239
+ item_keys = item["doc"]
240
+ if key_type == "doc" && item_keys[0..3] == "doc:"
241
+ keys << item_keys[4-item_keys.length..]
242
+ elsif key_type == "rev" && item_keys[0..3] == "rev:"
243
+ keys << item_keys[4-item_keys.length..]
244
+ end
245
+ end
246
+ end unless log.nil?
247
+
248
+ # return array
249
+ return [keys.uniq, ""]
250
+ end
251
+
169
252
  def self.sign(message, private_key, options)
170
253
  code, length, digest = multi_decode(private_key).first.unpack('SCa*')
171
254
  case Multicodecs[code].name
@@ -203,7 +286,7 @@ class Oydid
203
286
  end
204
287
  end
205
288
 
206
- def self.encrypt(message, public_key, options)
289
+ def self.encrypt(message, public_key, options = {})
207
290
  begin
208
291
  code, length, digest = multi_decode(public_key).first.unpack('CCa*')
209
292
  case Multicodecs[code].name
@@ -229,7 +312,7 @@ class Oydid
229
312
  end
230
313
  end
231
314
 
232
- def self.decrypt(message, private_key, options)
315
+ def self.decrypt(message, private_key, options = {})
233
316
  begin
234
317
  cipher = [JSON.parse(message)["value"]].pack('H*')
235
318
  nonce = [JSON.parse(message)["nonce"]].pack('H*')
@@ -250,10 +333,160 @@ class Oydid
250
333
  end
251
334
  end
252
335
 
336
+ # key="812b578d2e357270cbbd26a9dd44f93e5c8a3b44462e271348ce8f742dfe08144d06fd1f64d5a15c5b21564695d0dca9d65af322e8f96ef394400fe255d288cf"
337
+ def jweh(key)
338
+ pub_key=key[-64..-1]
339
+ prv_key=key[0..-65]
340
+
341
+ hex_pub=pub_key
342
+ bin_pub=[hex_pub].pack('H*')
343
+ int_pub=RbNaCl::PublicKey.new(bin_pub)
344
+ len_pub=int_pub.to_bytes.bytesize
345
+ enc_pub=Oydid.multi_encode([Multicodecs["x25519-pub"].code,len_pub,int_pub].pack("CCa#{len_pub}"),{}).first
346
+
347
+ hex_prv=prv_key
348
+ bin_prv=[hex_prv].pack('H*')
349
+ int_prv=RbNaCl::PrivateKey.new(bin_prv)
350
+ len_prv=int_prv.to_bytes.bytesize
351
+ enc_prv=Oydid.multi_encode([Multicodecs["ed25519-priv"].code,len_prv,int_prv].pack("SCa#{len_prv}"),{}).first
352
+ return [enc_pub, enc_prv]
353
+ end
354
+
355
+ # public_key=jweh(key).first
356
+ # require 'oydid'
357
+ # message="hallo"
358
+ # public_key="z6Mv4uEoFYJ369NoE9xUzxG5sm8KpPnvHX6YH6GsYFGSQ32J"
359
+ # jwe=Oydid.encryptJWE(message, public_key).first
360
+ def self.encryptJWE(message, public_key, options = {})
361
+
362
+ jwe_header = {"enc":"XC20P"}
363
+ recipient_alg = 'ECDH-ES+XC20PKW'
364
+
365
+ # Content Encryption ---
366
+ # random nonce for XChaCha20-Poly1305: uses a 192-bit nonce (24 bytes)
367
+ cnt_nnc = RbNaCl::Random.random_bytes(RbNaCl::AEAD::XChaCha20Poly1305IETF.nonce_bytes)
368
+ # random key for XChaCha20-Poly1305: uses a 256-bit key (32 bytes)
369
+ cnt_key = RbNaCl::Random.random_bytes(RbNaCl::AEAD::XChaCha20Poly1305IETF.key_bytes)
370
+ # addtional data
371
+ cnt_aad = jwe_header.to_json
372
+ # setup XChaCha20-Poly1305 for Authenticated Encryption with Associated Data (AEAD)
373
+ cnt_aead = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(cnt_key)
374
+ # encrypt
375
+ msg_enc = cnt_aead.encrypt(cnt_nnc, message, cnt_aad)
376
+ cnt_enc = msg_enc[0...-cnt_aead.tag_bytes]
377
+ cnt_tag = msg_enc[-cnt_aead.tag_bytes .. -1]
378
+
379
+ # Key Encryption ---
380
+ snd_prv = RbNaCl::PrivateKey.generate
381
+ code, length, digest = Oydid.multi_decode(public_key).first.unpack('CCa*')
382
+ buffer = RbNaCl::Util.zeros(RbNaCl::Boxes::Curve25519XSalsa20Poly1305::PublicKey::BYTES)
383
+ RbNaCl::Signatures::Ed25519::VerifyKey.crypto_sign_ed25519_pk_to_curve25519(buffer, digest)
384
+ shared_secret = RbNaCl::GroupElement.new(buffer).mult(snd_prv.to_bytes)
385
+ jwe_const = [0, 0, 0, 1] +
386
+ shared_secret.to_bytes.unpack('C*') +
387
+ [0,0,0,15] +
388
+ recipient_alg.bytes +
389
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
390
+ kek = RbNaCl::Hash.sha256(jwe_const.pack('C*'))
391
+ snd_nnc = RbNaCl::Random.random_bytes(RbNaCl::AEAD::XChaCha20Poly1305IETF.nonce_bytes)
392
+ snd_aead = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(kek)
393
+ snd_enc = snd_aead.encrypt(snd_nnc, cnt_key, nil)
394
+ snd_key = snd_enc[0...-snd_aead.tag_bytes]
395
+ snd_aut = snd_enc[-snd_aead.tag_bytes .. -1]
396
+
397
+ # create JWE ---
398
+ jwe_protected = Base64.urlsafe_encode64(jwe_header.to_json).delete("=")
399
+ jwe_encrypted_key = Base64.urlsafe_encode64(snd_key).delete("=")
400
+ jwe_init_vector = Base64.urlsafe_encode64(cnt_nnc).delete("=")
401
+ jwe_cipher_text = Base64.urlsafe_encode64(cnt_enc).delete("=")
402
+ jwe_auth_tag = Base64.urlsafe_encode64(cnt_tag).delete("=")
403
+ rcp_nnc_enc = Base64.urlsafe_encode64(snd_nnc).delete("=")
404
+ rcp_tag_enc = Base64.urlsafe_encode64(snd_aut).delete("=")
405
+
406
+ jwe_full = {
407
+ protected: jwe_protected,
408
+ iv: jwe_init_vector,
409
+ ciphertext: jwe_cipher_text,
410
+ tag: jwe_auth_tag,
411
+ recipients: [
412
+ {
413
+ encrypted_key: jwe_encrypted_key,
414
+ header: {
415
+ alg: recipient_alg,
416
+ iv: rcp_nnc_enc,
417
+ tag: rcp_tag_enc,
418
+ epk: {
419
+ kty: "OKP",
420
+ crv: "X25519",
421
+ x: Base64.urlsafe_encode64(snd_prv.public_key.to_bytes).delete("=")
422
+ }
423
+ }
424
+ }
425
+ ]
426
+ }
427
+
428
+ jwe = jwe_protected
429
+ jwe += "." + jwe_encrypted_key
430
+ jwe += "." + jwe_init_vector
431
+ jwe += "." + jwe_cipher_text
432
+ jwe += "." + jwe_auth_tag
433
+
434
+ return [jwe_full, ""]
435
+ end
436
+
437
+ # require 'oydid'
438
+ # message = '{"protected":"eyJlbmMiOiJYQzIwUCJ9","iv":"G24pK06HSbL0vTlRZMIEBLfa074tJ1tq","ciphertext":"N5bgUr8","tag":"CJeq8cuaercgoBZmrYoGUA","recipients":[{"encrypted_key":"chR8HQh1CRYRU4TdlfBbvon4fcb5PKfWPSo0SgkMC_8","header":{"alg":"ECDH-ES+XC20PKW","iv":"K7lUo8shyJhxC7Nl45VlXes4tbDeZyBL","tag":"2sLCGRv70ESqEAqos3ZhSg","epk":{"kty":"OKP","crv":"X25519","x":"ZpnKcI7Kac6HPwVAGwM0PBweTFKM6wHHVljTHMRWpD4"}}}]}'
439
+ # message = jwe.to_json
440
+ # private_key = "z1S5USmDosHvi2giCLHCCgcq3Cd31mrhMcUy2fcfszxxLkD7"
441
+ # Oydid.decryptJWE(jwe.to_json, private_key)
442
+ def self.decryptJWE(message, private_key, options = {})
443
+
444
+ # JWE parsing
445
+ jwe_full = JSON.parse(message)
446
+ snd_pub_enc = jwe_full["recipients"].first["header"]["epk"]["x"]
447
+ snd_key_enc = jwe_full["recipients"].first["encrypted_key"]
448
+ snd_nnc_enc = jwe_full["recipients"].first["header"]["iv"]
449
+ snd_tag_enc = jwe_full["recipients"].first["header"]["tag"]
450
+ cnt_cip_enc = jwe_full["ciphertext"]
451
+ cnt_tag_enc = jwe_full["tag"]
452
+ cnt_nnc_enc = jwe_full["iv"]
453
+ cnt_aad_enc = jwe_full["protected"]
454
+ recipient_alg = jwe_full["recipients"].first["header"]["alg"]
455
+
456
+ snd_pub = Base64.urlsafe_decode64(snd_pub_enc)
457
+ snd_nnc = Base64.urlsafe_decode64(snd_nnc_enc)
458
+ snd_key = Base64.urlsafe_decode64(snd_key_enc)
459
+ snd_tag = Base64.urlsafe_decode64(snd_tag_enc)
460
+ cnt_nnc = Base64.urlsafe_decode64(cnt_nnc_enc)
461
+ cnt_cip = Base64.urlsafe_decode64(cnt_cip_enc)
462
+ cnt_tag = Base64.urlsafe_decode64(cnt_tag_enc)
463
+ cnt_aad = Base64.urlsafe_decode64(cnt_aad_enc)
464
+
465
+ # Key Decryption
466
+ code, length, digest = Oydid.multi_decode(private_key).first.unpack('SCa*')
467
+ buffer = RbNaCl::Util.zeros(RbNaCl::Boxes::Curve25519XSalsa20Poly1305::PublicKey::BYTES)
468
+ RbNaCl::Signatures::Ed25519::SigningKey.crypto_sign_ed25519_sk_to_curve25519(buffer, digest)
469
+ shared_secret = RbNaCl::GroupElement.new(snd_pub).mult(buffer)
470
+ jwe_const = [0, 0, 0, 1] +
471
+ shared_secret.to_bytes.unpack('C*') +
472
+ [0,0,0,15] +
473
+ recipient_alg.bytes +
474
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
475
+ kek = RbNaCl::Hash.sha256(jwe_const.pack('C*'))
476
+ snd_aead = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(kek)
477
+ cnt_key = snd_aead.decrypt(snd_nnc, snd_key+snd_tag, nil)
478
+
479
+ # Content Decryption
480
+ cnt_aead = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(cnt_key)
481
+ cnt_dec = cnt_aead.decrypt(cnt_nnc, cnt_cip+cnt_tag, cnt_aad)
482
+
483
+ return [cnt_dec, ""]
484
+ end
485
+
253
486
  def self.read_private_key(filename, options)
254
487
  begin
255
488
  f = File.open(filename)
256
- key_encoded = f.read
489
+ key_encoded = f.read.strip
257
490
  f.close
258
491
  rescue
259
492
  return [nil, "cannot read file"]
@@ -284,6 +517,9 @@ class Oydid
284
517
  when 'ed25519-pub'
285
518
  verify_key = Ed25519::VerifyKey.new(digest)
286
519
  return [verify_key, ""]
520
+ when 'x25519-pub'
521
+ pub_key = RbNaCl::PublicKey.new(digest)
522
+ return [pub_key, ""]
287
523
  else
288
524
  return [nil, "unsupported key codec"]
289
525
  end
@@ -331,9 +567,16 @@ class Oydid
331
567
  case doc_location
332
568
  when /^http/
333
569
  doc_location = doc_location.sub("%3A%2F%2F","://").sub("%3A", ":")
334
- retVal = HTTParty.get(doc_location + "/doc/" + doc_identifier)
570
+ option_str = ""
571
+ if options[:followAlsoKnownAs]
572
+ option_str = "?followAlsoKnownAs=true"
573
+ end
574
+ retVal = HTTParty.get(doc_location + "/doc/" + doc_identifier + option_str)
335
575
  if retVal.code != 200
336
- msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc/" + doc_identifier.to_s
576
+ msg = retVal.parsed_response["error"].to_s rescue ""
577
+ if msg.to_s == ""
578
+ msg = "invalid response from " + doc_location.to_s + "/doc/" + doc_identifier.to_s
579
+ end
337
580
  return [nil, msg]
338
581
  end
339
582
  if options.transform_keys(&:to_s)["trace"]
data/lib/oydid/didcomm.rb CHANGED
@@ -102,12 +102,12 @@ class Oydid
102
102
  # DID Auth for data container with challenge ---
103
103
  def self.token_from_challenge(host, pwd, options = {})
104
104
  sid = SecureRandom.hex(20).to_s
105
+ public_key = public_key(generate_private_key(pwd, options).first, options).first
105
106
  retVal = HTTParty.post(host + "/oydid/init",
106
107
  headers: { 'Content-Type' => 'application/json' },
107
- body: { "session_id": sid }.to_json )
108
+ body: { "session_id": sid, "public_key": public_key }.to_json )
108
109
  challenge = retVal.parsed_response["challenge"]
109
110
  signed_challenge = sign(challenge, Oydid.generate_private_key(pwd, options).first, options).first
110
- public_key = public_key(generate_private_key(pwd, options).first, options).first
111
111
  retVal = HTTParty.post(host + "/oydid/token",
112
112
  headers: { 'Content-Type' => 'application/json' },
113
113
  body: {