ruby-safenet 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4557bfe89264b253153460ccc8c69ddc7326bacd
4
+ data.tar.gz: 0fac6e8ff9d6290792e5f78bc93367e5e5d8ed5a
5
+ SHA512:
6
+ metadata.gz: 2eb2c81e7041b0519caf8519915d184c7b1e93e6c3a916c34a93754b1bb22147f9c7bd186b19cc152a3ab5b0520f8642bda9d33f8989d80e4f85f378598bd3a3
7
+ data.tar.gz: 3dcf2664123244491c1adc5c089162172159029d5ed318c13cfa2b7b3a6d8e68a7708a6e2bdc5912e0b301960f61675706ac64870d56a3509be4227283a06aab
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in safenet.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Ruby-Safenet
2
+
3
+ A Ruby library for accessing the SAFE network
4
+
5
+ ## Installation
6
+
7
+ $ gem install safenet
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ require "safenet"
13
+
14
+ SafeNet.set_app_info({
15
+ name: "Demo App",
16
+ version: "0.0.1",
17
+ vendor: "maidsafe",
18
+ id: "org.maidsafe.demo",
19
+ })
20
+
21
+ SafeNet.create_directory("/mydir")
22
+ SafeNet.file("/mydir/index.html")
23
+ SafeNet.update_file_content("/mydir/index.html", "Hello world!<br>I'm a webpage :D")
24
+ SafeNet.register_service("mywonderfulapp", "www", "/mydir")
25
+ SafeNet.get_file("/mydir/index.html")
26
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "safenet"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module Safenet
2
+ VERSION = "0.0.1"
3
+ end
data/lib/safenet.rb ADDED
@@ -0,0 +1,423 @@
1
+ require "safenet/version"
2
+
3
+ module SafeNet
4
+ require "net/http"
5
+ require "rbnacl"
6
+ require "base64"
7
+ require "json"
8
+ require "cgi" # CGI.escape method
9
+
10
+ # default values
11
+ @@NAME = "Demo App"
12
+ @@VERSION = "0.0.1"
13
+ @@VENDOR = "maidsafe"
14
+ @@ID = "org.maidsafe.demo"
15
+ @@LAUCHER_SERVER = "http://localhost:8100/"
16
+ @@CONF_PATH = File.join(File.expand_path('.', __FILE__), "conf.json")
17
+
18
+ def self.set_app_info(options)
19
+ @@NAME = options[:name] if options.has_key?(:name)
20
+ @@VERSION = options[:version] if options.has_key?(:version)
21
+ @@VENDOR = options[:vendor] if options.has_key?(:vendor)
22
+ @@ID = options[:id] if options.has_key?(:id)
23
+ @@LAUCHER_SERVER = options[:server] if options.has_key?(:server)
24
+ @@CONF_PATH = options[:conf_file] if options.has_key?(:conf_file)
25
+ end
26
+
27
+ def self.auth
28
+ url = "#{@@LAUCHER_SERVER}auth"
29
+
30
+ private_key = RbNaCl::PrivateKey.generate
31
+ nonce = RbNaCl::Random.random_bytes(24)
32
+
33
+ payload = {
34
+ app: {
35
+ name: @@NAME,
36
+ version: @@VERSION,
37
+ vendor: @@VENDOR,
38
+ id: @@ID
39
+ },
40
+ publicKey: Base64.strict_encode64(private_key.public_key),
41
+ nonce: Base64.strict_encode64(nonce),
42
+ permissions: []
43
+ }
44
+
45
+ uri = URI(url)
46
+ http = Net::HTTP.new(uri.host, uri.port)
47
+ req = Net::HTTP::Post.new(uri.path, {'Content-Type' => 'application/json'})
48
+ req.body = payload.to_json
49
+ res = http.request(req)
50
+
51
+ if res.code == "200"
52
+ response = JSON.parse(res.body)
53
+
54
+ # save it in conf.json
55
+ conf = response.dup
56
+ conf["nonce"] = Base64.strict_encode64(nonce)
57
+ conf["privateKey"] = Base64.strict_encode64(private_key)
58
+ File.open(@@CONF_PATH, "w") { |f| f << JSON.pretty_generate(conf) }
59
+
60
+ else
61
+ # puts "ERROR #{res.code}: #{res.message}"
62
+ response = nil
63
+ end
64
+
65
+ response
66
+ end
67
+
68
+
69
+ def self.is_token_valid
70
+ url = "#{@@LAUCHER_SERVER}auth"
71
+
72
+ uri = URI(url)
73
+ http = Net::HTTP.new(uri.host, uri.port)
74
+ req = Net::HTTP::Get.new(uri.path, {
75
+ 'Authorization' => "Bearer #{self.get_token()}"
76
+ })
77
+ res = http.request(req)
78
+ res.code == "200"
79
+ end
80
+
81
+
82
+ def self.revoke_token
83
+ url = "#{@@LAUCHER_SERVER}auth"
84
+
85
+ uri = URI(url)
86
+ http = Net::HTTP.new(uri.host, uri.port)
87
+ req = Net::HTTP::Delete.new(uri.path, {
88
+ 'Authorization' => "Bearer #{self.get_valid_token()}"
89
+ })
90
+ res = http.request(req)
91
+ res.code == "200"
92
+ end
93
+
94
+ def self.get_directory(dir_path, options = {})
95
+ # default values
96
+ options[:is_path_shared] ||= false
97
+
98
+ # entry point
99
+ url = "#{@@LAUCHER_SERVER}nfs/directory/#{CGI.escape(dir_path)}/#{options[:is_path_shared]}"
100
+
101
+ uri = URI(url)
102
+ http = Net::HTTP.new(uri.host, uri.port)
103
+ req = Net::HTTP::Get.new(uri.path, {
104
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
105
+ })
106
+ res = http.request(req)
107
+ JSON.parse(self.decrypt(res.body))
108
+ end
109
+
110
+
111
+ # options = is_private, metadata, is_versioned, is_path_shared
112
+ def self.file(file_path, options = {})
113
+ url = "#{@@LAUCHER_SERVER}nfs/file"
114
+
115
+ # default values
116
+ options[:is_private] ||= true
117
+ options[:is_versioned] ||= false
118
+ options[:is_path_shared] ||= false
119
+
120
+ # payload
121
+ payload = {
122
+ filePath: file_path,
123
+ isPrivate: options[:is_private],
124
+ isVersioned: options[:is_versioned],
125
+ isPathShared: options[:is_path_shared]
126
+ }
127
+
128
+ # optional
129
+ payload["metadata"] = options[:metadata] if options.has_key?(:metadata)
130
+
131
+ # api call
132
+ uri = URI(url)
133
+ http = Net::HTTP.new(uri.host, uri.port)
134
+ req = Net::HTTP::Post.new(uri.path, {
135
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
136
+ 'Content-Type' => 'text/plain'
137
+ })
138
+ req.body = self.encrypt(payload.to_json)
139
+ res = http.request(req)
140
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
141
+ end
142
+
143
+
144
+ # options = is_private, metadata, is_versioned, is_path_shared
145
+ # ex.: create_directory("/photos")
146
+ def self.create_directory(dir_path, options = {})
147
+ url = "#{@@LAUCHER_SERVER}nfs/directory"
148
+
149
+ # default values
150
+ options[:is_private] ||= true
151
+ options[:is_versioned] ||= false
152
+ options[:is_path_shared] ||= false
153
+
154
+ # payload
155
+ payload = {
156
+ dirPath: dir_path,
157
+ isPrivate: options[:is_private],
158
+ isVersioned: options[:is_versioned],
159
+ isPathShared: options[:is_path_shared]
160
+ }
161
+
162
+ # optional
163
+ payload["metadata"] = options[:metadata] if options.has_key?(:metadata)
164
+
165
+ # api call
166
+ uri = URI(url)
167
+ http = Net::HTTP.new(uri.host, uri.port)
168
+ req = Net::HTTP::Post.new(uri.path, {
169
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
170
+ 'Content-Type' => 'text/plain'
171
+ })
172
+ req.body = self.encrypt(payload.to_json)
173
+ res = http.request(req)
174
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
175
+ end
176
+
177
+
178
+ # ex.: delete_directory("/photos")
179
+ def self.delete_directory(dir_path, options = {})
180
+ # default values
181
+ options[:is_path_shared] ||= false
182
+
183
+ # entry point
184
+ url = "#{@@LAUCHER_SERVER}nfs/directory/#{CGI.escape(dir_path)}/#{options[:is_path_shared]}"
185
+
186
+ # api call
187
+ uri = URI(url)
188
+ http = Net::HTTP.new(uri.host, uri.port)
189
+ req = Net::HTTP::Delete.new(uri.path, {
190
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
191
+ })
192
+ res = http.request(req)
193
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
194
+ end
195
+
196
+ # options: offset, length, is_path_shared
197
+ def self.get_file(file_path, options = {})
198
+ # default values
199
+ options[:offset] ||= 0
200
+ options[:is_path_shared] ||= false
201
+
202
+ # entry point
203
+ url = "#{@@LAUCHER_SERVER}nfs/file/#{CGI.escape(file_path)}/#{options[:is_path_shared]}?"
204
+
205
+ # query params are encrypted
206
+ query = []
207
+ query << "offset=#{options[:offset]}"
208
+ query << "length=#{options[:length]}" if options.has_key?(:length) # length is optional
209
+ url = "#{url}#{self.encrypt(query.join('&'))}"
210
+
211
+ # api call
212
+ uri = URI(url)
213
+ http = Net::HTTP.new(uri.host, uri.port)
214
+ req = Net::HTTP::Get.new(uri.path, {
215
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
216
+ })
217
+ res = http.request(req)
218
+ res.code == "200" ? self.decrypt(res.body) : JSON.parse(self.decrypt(res.body))
219
+ end
220
+
221
+ def self.update_file_content(file_path, contents, options = {})
222
+ # default values
223
+ options[:offset] ||= 0
224
+ options[:is_path_shared] ||= false
225
+
226
+ # entry point
227
+ url = "#{@@LAUCHER_SERVER}nfs/file/#{CGI.escape(file_path)}/#{options[:is_path_shared]}?offset=#{options[:offset]}"
228
+
229
+ # api call
230
+ uri = URI(url)
231
+ http = Net::HTTP.new(uri.host, uri.port)
232
+ req = Net::HTTP::Put.new(uri.path, {
233
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
234
+ 'Content-Type' => 'text/plain'
235
+ })
236
+ req.body = self.encrypt(contents)
237
+ res = http.request(req)
238
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
239
+ end
240
+
241
+ def self.delete_file(file_path, options = {})
242
+ # default values
243
+ options[:is_path_shared] ||= false
244
+
245
+ # entry point
246
+ url = "#{@@LAUCHER_SERVER}nfs/file/#{CGI.escape(file_path)}/#{options[:is_path_shared]}"
247
+
248
+ # api call
249
+ uri = URI(url)
250
+ http = Net::HTTP.new(uri.host, uri.port)
251
+ req = Net::HTTP::Delete.new(uri.path, {
252
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
253
+ })
254
+ res = http.request(req)
255
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
256
+ end
257
+
258
+
259
+ def self.create_long_name(long_name)
260
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(long_name)}"
261
+
262
+ # api call
263
+ uri = URI(url)
264
+ http = Net::HTTP.new(uri.host, uri.port)
265
+ req = Net::HTTP::Post.new(uri.path, {
266
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
267
+ 'Content-Type' => 'text/plain'
268
+ })
269
+ res = http.request(req)
270
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
271
+ end
272
+
273
+
274
+ # ex.: register_service("thegoogle", "www", "/www")
275
+ def self.register_service(long_name, service_name, service_home_dir_path, options = {})
276
+ url = "#{@@LAUCHER_SERVER}dns"
277
+
278
+ # default values
279
+ options[:is_path_shared] ||= false
280
+
281
+ # payload
282
+ payload = {
283
+ longName: long_name,
284
+ serviceName: service_name,
285
+ serviceHomeDirPath: service_home_dir_path,
286
+ isPathShared: options[:is_path_shared]
287
+ }
288
+
289
+ # optional
290
+ payload["metadata"] = options[:metadata] if options.has_key?(:metadata)
291
+
292
+ # api call
293
+ uri = URI(url)
294
+ http = Net::HTTP.new(uri.host, uri.port)
295
+ req = Net::HTTP::Post.new(uri.path, {
296
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
297
+ 'Content-Type' => 'text/plain'
298
+ })
299
+ req.body = self.encrypt(payload.to_json)
300
+ res = http.request(req)
301
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
302
+ end
303
+
304
+
305
+ def self.list_long_names
306
+ url = "#{@@LAUCHER_SERVER}dns"
307
+
308
+ # api call
309
+ uri = URI(url)
310
+ http = Net::HTTP.new(uri.host, uri.port)
311
+ req = Net::HTTP::Get.new(uri.path, {
312
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
313
+ })
314
+ res = http.request(req)
315
+ JSON.parse(self.decrypt(res.body))
316
+ end
317
+
318
+
319
+ def self.list_services(long_name)
320
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(long_name)}"
321
+
322
+ # api call
323
+ uri = URI(url)
324
+ http = Net::HTTP.new(uri.host, uri.port)
325
+ req = Net::HTTP::Get.new(uri.path, {
326
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
327
+ })
328
+ res = http.request(req)
329
+ JSON.parse(self.decrypt(res.body))
330
+ end
331
+
332
+
333
+ def self.get_home_dir(long_name, service_name)
334
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(service_name)}/#{CGI.escape(long_name)}"
335
+
336
+ # api call
337
+ uri = URI(url)
338
+ http = Net::HTTP.new(uri.host, uri.port)
339
+ req = Net::HTTP::Get.new(uri.path)
340
+ res = http.request(req)
341
+ JSON.parse(res.body)
342
+ end
343
+
344
+
345
+ # get_file_unauth("thegoogle", "www", "index.html", offset: 3, length: 5)
346
+ def self.get_file_unauth(long_name, service_name, file_path, options = {})
347
+ # default values
348
+ options[:offset] ||= 0
349
+
350
+ # entry point
351
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(service_name)}/#{CGI.escape(long_name)}/#{CGI.escape(file_path)}?"
352
+
353
+ # query params are encrypted
354
+ query = []
355
+ query << "offset=#{options[:offset]}"
356
+ query << "length=#{options[:length]}" if options.has_key?(:length) # length is optional
357
+ url = "#{url}#{self.encrypt(query.join('&'))}"
358
+
359
+ # api call
360
+ uri = URI(url)
361
+ http = Net::HTTP.new(uri.host, uri.port)
362
+ req = Net::HTTP::Get.new(uri.path)
363
+ res = http.request(req)
364
+ res.code == "200" ? res.body : JSON.parse(res.body)
365
+ end
366
+
367
+ private
368
+
369
+ def self.get_token
370
+ @@conf = File.exists?(@@CONF_PATH) ? JSON.parse(File.read(@@CONF_PATH)) : (self.auth() || {})
371
+ @@conf["token"]
372
+ end
373
+
374
+ def self.get_valid_token
375
+ @last_conf = File.exists?(@@CONF_PATH) ? JSON.parse(File.read(@@CONF_PATH)) : {}
376
+ self.auth() unless File.exists?(@@CONF_PATH) && self.is_token_valid()
377
+ @@conf = JSON.parse(File.read(@@CONF_PATH))
378
+ @@conf["token"]
379
+ end
380
+
381
+
382
+ def self.get_keys
383
+ @@keys ||= {}
384
+
385
+ # not loaded yet?
386
+ if @@keys.empty?
387
+ @@conf ||= {}
388
+ self.get_valid_token if @@conf.empty?
389
+
390
+ # extract keys
391
+ cipher_text = Base64.strict_decode64(@@conf["encryptedKey"])
392
+ nonce = Base64.strict_decode64(@@conf["nonce"])
393
+ private_key = Base64.strict_decode64(@@conf["privateKey"])
394
+ public_key = Base64.strict_decode64(@@conf["publicKey"])
395
+
396
+ box = RbNaCl::Box.new(public_key, private_key)
397
+ data = box.decrypt(nonce, cipher_text)
398
+
399
+ # The first segment of the data will have the symmetric key
400
+ @@keys["symmetric_key"] = data.slice(0, RbNaCl::SecretBox.key_bytes)
401
+
402
+ # The second segment of the data will have the nonce to be used
403
+ @@keys["symmetric_nonce"] = data.slice(RbNaCl::SecretBox.key_bytes, RbNaCl::SecretBox.key_bytes)
404
+
405
+ # keep the box object in cache
406
+ @@keys["secret_box"] = RbNaCl::SecretBox.new(@@keys["symmetric_key"])
407
+ end
408
+
409
+ @@keys
410
+ end
411
+
412
+ def self.decrypt(message_base64)
413
+ keys = self.get_keys
414
+ keys["secret_box"].decrypt(keys["symmetric_nonce"], Base64.strict_decode64(message_base64))
415
+ end
416
+
417
+ def self.encrypt(message)
418
+ keys = self.get_keys
419
+ res = keys["secret_box"].encrypt(keys["symmetric_nonce"], message)
420
+ Base64.strict_encode64(res)
421
+ end
422
+
423
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'safenet/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ruby-safenet"
8
+ spec.version = Safenet::VERSION
9
+ spec.authors = ["Daniel Loureiro"]
10
+ spec.email = ["loureirorg@gmail.com"]
11
+
12
+ spec.summary = %q{Ruby library for accessing the SAFE network}
13
+ spec.description = %q{Ruby library for accessing the SAFE network}
14
+ spec.homepage = "https://github.com/loureirorg/ruby-safenet"
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ # if spec.respond_to?(:metadata)
19
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
20
+ # else
21
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ # end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.10"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_runtime_dependency "rbnacl", "~> 3.3"
32
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-safenet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Loureiro
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rbnacl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.3'
55
+ description: Ruby library for accessing the SAFE network
56
+ email:
57
+ - loureirorg@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - README.md
66
+ - Rakefile
67
+ - bin/console
68
+ - bin/setup
69
+ - lib/safenet.rb
70
+ - lib/safenet/version.rb
71
+ - ruby-safenet.gemspec
72
+ homepage: https://github.com/loureirorg/ruby-safenet
73
+ licenses: []
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.4.8
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Ruby library for accessing the SAFE network
95
+ test_files: []
96
+ has_rdoc: