duse 0.0.2

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 (61) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +35 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +13 -0
  6. data/LICENSE +22 -0
  7. data/README.md +89 -0
  8. data/Rakefile +11 -0
  9. data/bin/duse +8 -0
  10. data/duse.gemspec +22 -0
  11. data/lib/duse.rb +10 -0
  12. data/lib/duse/cli.rb +104 -0
  13. data/lib/duse/cli/account.rb +20 -0
  14. data/lib/duse/cli/account_confirm.rb +22 -0
  15. data/lib/duse/cli/account_info.rb +18 -0
  16. data/lib/duse/cli/account_password.rb +14 -0
  17. data/lib/duse/cli/account_password_change.rb +30 -0
  18. data/lib/duse/cli/account_password_reset.rb +20 -0
  19. data/lib/duse/cli/account_resend_confirmation.rb +20 -0
  20. data/lib/duse/cli/account_update.rb +25 -0
  21. data/lib/duse/cli/api_command.rb +33 -0
  22. data/lib/duse/cli/cli_config.rb +54 -0
  23. data/lib/duse/cli/command.rb +202 -0
  24. data/lib/duse/cli/config.rb +16 -0
  25. data/lib/duse/cli/help.rb +23 -0
  26. data/lib/duse/cli/key_helper.rb +84 -0
  27. data/lib/duse/cli/login.rb +27 -0
  28. data/lib/duse/cli/meta_command.rb +12 -0
  29. data/lib/duse/cli/parser.rb +43 -0
  30. data/lib/duse/cli/password_helper.rb +17 -0
  31. data/lib/duse/cli/register.rb +45 -0
  32. data/lib/duse/cli/secret.rb +19 -0
  33. data/lib/duse/cli/secret_add.rb +38 -0
  34. data/lib/duse/cli/secret_generator.rb +10 -0
  35. data/lib/duse/cli/secret_get.rb +40 -0
  36. data/lib/duse/cli/secret_list.rb +20 -0
  37. data/lib/duse/cli/secret_remove.rb +19 -0
  38. data/lib/duse/cli/secret_update.rb +44 -0
  39. data/lib/duse/cli/share_with_user.rb +53 -0
  40. data/lib/duse/cli/version.rb +12 -0
  41. data/lib/duse/client/config.rb +36 -0
  42. data/lib/duse/client/entity.rb +87 -0
  43. data/lib/duse/client/namespace.rb +112 -0
  44. data/lib/duse/client/secret.rb +69 -0
  45. data/lib/duse/client/session.rb +128 -0
  46. data/lib/duse/client/user.rb +23 -0
  47. data/lib/duse/encryption.rb +39 -0
  48. data/lib/duse/version.rb +3 -0
  49. data/spec/cli/cli_config_spec.rb +49 -0
  50. data/spec/cli/commands/account_spec.rb +45 -0
  51. data/spec/cli/commands/config_spec.rb +17 -0
  52. data/spec/cli/commands/login_spec.rb +51 -0
  53. data/spec/cli/commands/register_spec.rb +38 -0
  54. data/spec/cli/commands/secret_spec.rb +142 -0
  55. data/spec/client/secret_marshaller_spec.rb +32 -0
  56. data/spec/client/secret_spec.rb +96 -0
  57. data/spec/client/user_spec.rb +105 -0
  58. data/spec/spec_helper.rb +70 -0
  59. data/spec/support/helpers.rb +43 -0
  60. data/spec/support/mock_api.rb +142 -0
  61. metadata +159 -0
@@ -0,0 +1,87 @@
1
+ module Duse
2
+ module Client
3
+ class Entity
4
+ MAP = {}
5
+
6
+ def self.base_path
7
+ many
8
+ end
9
+
10
+ def self.subclasses
11
+ MAP.values.uniq
12
+ end
13
+
14
+ def self.one(key = nil)
15
+ MAP[key.to_s] = self if key
16
+ @one ||= key.to_s
17
+ end
18
+
19
+ def self.many(key = nil)
20
+ MAP[key.to_s] = self if key
21
+ @many ||= key.to_s
22
+ end
23
+
24
+ def self.attributes(*list)
25
+ @attributes ||= []
26
+
27
+ list.each do |name|
28
+ add_attribute name.to_s
29
+ end
30
+
31
+ @attributes
32
+ end
33
+ self.singleton_class.send :alias_method, :has, :attributes
34
+
35
+ def self.add_attribute(name)
36
+ dummy = self.new
37
+
38
+ attributes << name
39
+ define_method(name) { load_attribute(name) } unless dummy.respond_to? name
40
+ define_method("#{name}=") { |value| set_attribute(name, value) } unless dummy.respond_to? "#{name}="
41
+ define_method("#{name}?") { !!send(name) } unless dummy.respond_to? "#{name}?"
42
+ end
43
+
44
+ def self.id_field(key = nil)
45
+ @id_field = key.to_s if key
46
+ @id_field
47
+ end
48
+
49
+ attr_accessor :curry
50
+ attr_reader :attributes
51
+ alias_method :to_h, :attributes
52
+
53
+ def initialize(options = {})
54
+ @attributes = {}
55
+ options.each do |key, value|
56
+ self.send("#{key.to_s}=", value) if respond_to? "#{key.to_s}="
57
+ end
58
+ end
59
+
60
+ def set_attribute(name, value)
61
+ attributes[name.to_s] = value
62
+ end
63
+
64
+ def load_attribute(name)
65
+ reload if missing? name
66
+ attributes[name.to_s]
67
+ end
68
+
69
+ def reload
70
+ attributes.merge! curry.find_one(id).attributes
71
+ end
72
+
73
+ def save
74
+ fail NotImplementedError, 'Save will be the "update" action, once the api supports it'
75
+ end
76
+
77
+ def delete
78
+ curry.delete id
79
+ end
80
+
81
+ def missing?(name)
82
+ return false unless self.class.attributes.include? name
83
+ !attributes.key?(name)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,112 @@
1
+ require 'duse/client/entity'
2
+ require 'duse/client/session'
3
+
4
+ module Duse
5
+ module Client
6
+ class Namespace < Module
7
+ class Curry < Module
8
+ attr_reader :namespace, :type
9
+
10
+ def initialize(namespace, type)
11
+ @namespace, @type = namespace, type
12
+ end
13
+
14
+ def find_one(id)
15
+ entity = session.find_one(type, id)
16
+ entity.curry = self
17
+ entity
18
+ end
19
+ alias_method :find, :find_one
20
+ alias_method :get, :find_one
21
+
22
+ def create(params)
23
+ entity = session.create(type, params)
24
+ entity.curry = self
25
+ entity
26
+ end
27
+
28
+ def update(id, params)
29
+ entity = session.update(type, id, params)
30
+ entity.curry = self
31
+ entity
32
+ end
33
+
34
+ def delete(id)
35
+ session.delete_one(type, id)
36
+ end
37
+
38
+ def find_many(params = {})
39
+ session.find_many(type, params).each do |e|
40
+ e.curry = self
41
+ end
42
+ end
43
+ alias_method :find_all, :find_many
44
+ alias_method :all, :find_all
45
+
46
+ private
47
+
48
+ def session
49
+ namespace.session
50
+ end
51
+
52
+ module User
53
+ def current
54
+ find_one 'me'
55
+ end
56
+
57
+ def server
58
+ find_one 'server'
59
+ end
60
+
61
+ def confirm(token)
62
+ session.patch('/users/confirm', { token: token })
63
+ end
64
+
65
+ def password_reset(token, password)
66
+ session.patch('/users/password', {
67
+ token: token,
68
+ password: password
69
+ })
70
+ end
71
+
72
+ def forgot_password(email)
73
+ session.post('/users/forgot_password', { email: self.email })
74
+ end
75
+
76
+ def resend_confirmation(email)
77
+ session.post('/users/confirm', { email: self.email })
78
+ end
79
+ end
80
+ end
81
+
82
+ attr_accessor :session
83
+
84
+ def initialize
85
+ @session = Session.new
86
+
87
+ Entity.subclasses.each do |subclass|
88
+ name = subclass.name[/[^:]+$/]
89
+ curry = Curry.new(self, subclass)
90
+ curry_extension = get_curry_extension(name)
91
+ curry.extend curry_extension if curry_extension
92
+ const_set(name, curry)
93
+ end
94
+ end
95
+
96
+ def get_curry_extension(name)
97
+ Curry.const_get(name)
98
+ rescue NameError
99
+ nil
100
+ end
101
+
102
+ def included(klass)
103
+ return if klass == Object or klass == Kernel
104
+ namespace = self
105
+ klass.define_singleton_method(:session) { namespace.session }
106
+ klass.define_singleton_method(:session=) { |value| namespace.session = value }
107
+ klass.define_singleton_method(:config) { session.config }
108
+ klass.define_singleton_method(:config=) { |value| session.config = value }
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,69 @@
1
+ require 'duse/client/entity'
2
+ require 'duse/encryption'
3
+ require 'secret_sharing'
4
+
5
+ module Duse
6
+ module Client
7
+ class SecretMarshaller
8
+ def initialize(secret, private_key)
9
+ @secret = secret
10
+ @private_key = private_key
11
+ end
12
+
13
+ def to_h
14
+ secret_hash = {}
15
+ secret_hash['title'] = @secret.title if @secret.title
16
+ secret_hash['parts'] = parts_from_secret if @secret.secret_text
17
+ secret_hash
18
+ end
19
+
20
+ def parts_from_secret
21
+ # sliced of 18 is a result of trial & error, if it's too large then
22
+ # encryption will fail. Might improve with: http://stackoverflow.com/questions/11505547/how-calculate-size-of-rsa-cipher-text-using-key-size-clear-text-length
23
+ secret_text_in_slices_of(18).map do |secret_part|
24
+ shares = SecretSharing.split_secret(secret_part, 2, @secret.users.length)
25
+ @secret.users.each_with_index.map do |user, index|
26
+ share = shares[index]
27
+ content, signature = Duse::Encryption.encrypt(@private_key, user.public_key, share)
28
+ {"user_id" => user.id, "content" => content, "signature" => signature}
29
+ end
30
+ end
31
+ end
32
+
33
+ def secret_text_in_slices_of(piece_size)
34
+ encoded_secret = Encryption.encode(@secret.secret_text)
35
+ encoded_secret.chars.each_slice(piece_size).map(&:join)
36
+ end
37
+ end
38
+
39
+ class Secret < Entity
40
+ attributes :id, :title, :parts
41
+ has :users
42
+
43
+ attr_accessor :secret_text
44
+
45
+ id_field :id
46
+ one :secret
47
+ many :secrets
48
+
49
+ def decrypt(private_key)
50
+ unless self.secret_text
51
+ secret_text = parts(private_key).inject('') do |result, shares|
52
+ result << SecretSharing.recover_secret(shares)
53
+ end
54
+ self.secret_text = Encryption.decode(secret_text)
55
+ end
56
+ self.secret_text
57
+ end
58
+
59
+ def parts(private_key)
60
+ return nil if load_attribute('parts').nil?
61
+ load_attribute('parts').map do |part|
62
+ part.map do |share|
63
+ Duse::Encryption.decrypt private_key, share
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,128 @@
1
+ require 'duse/client/entity'
2
+ require 'faraday'
3
+ require 'faraday_middleware'
4
+
5
+ module Duse
6
+ module Client
7
+ class Error < StandardError; end
8
+ class SSLError < Error; end
9
+ class NotLoggedIn < Error; end
10
+ class NotAuthorized < Error; end
11
+ class NotFound < Error; end
12
+ class ValidationFailed < Error; end
13
+
14
+ class Session
15
+ attr_accessor :config
16
+
17
+ def find_one(entity, id)
18
+ response_body = get("/#{entity.base_path}/#{id}")
19
+ instance_from(entity, response_body)
20
+ end
21
+
22
+ def find_many(entity, params = {})
23
+ response_body = get("/#{entity.base_path}")
24
+ instances_from(entity, response_body)
25
+ end
26
+
27
+ def create(entity, hash)
28
+ response_body = post("/#{entity.base_path}", hash)
29
+ instance_from(entity, response_body)
30
+ end
31
+
32
+ def update(entity, id, hash)
33
+ response_body = patch("/#{entity.base_path}/#{id}", hash)
34
+ instance_from(entity, response_body)
35
+ end
36
+
37
+ def delete_one(entity, id)
38
+ delete("/#{entity.base_path}/#{id}")
39
+ end
40
+
41
+ def get(*args)
42
+ raw(:get, *args)
43
+ end
44
+
45
+ def post(*args)
46
+ raw(:post, *args)
47
+ end
48
+
49
+ def patch(*args)
50
+ raw(:patch, *args)
51
+ end
52
+
53
+ def delete(*args)
54
+ raw(:delete, *args)
55
+ end
56
+
57
+ def raw(*args)
58
+ process_raw_http_response raw_http_request(*args)
59
+ end
60
+
61
+ def process_raw_http_response(response)
62
+ case response.status
63
+ when 0 then raise SSLError, 'SSL error: could not verify peer'
64
+ when 200..299 then JSON.parse(response.body) rescue response.body
65
+ when 301, 303 then raw(:get, response.headers['Location'])
66
+ when 302, 307, 308 then raw(verb, response.headers['Location'])
67
+ when 401 then raise NotLoggedIn, error_msg(response.body)
68
+ when 403 then raise NotAuthorized, error_msg(response.body)
69
+ when 404 then raise NotFound, error_msg(response.body)
70
+ when 422 then raise ValidationFailed, error_msg(response.body)
71
+ when 400..499 then raise Error, error_msg(response.body)
72
+ when 500..599 then raise Error, error_msg(response.body)
73
+ else raise Error, "unhandled status code #{response.status}"
74
+ end
75
+ end
76
+
77
+ def raw_http_request(*args)
78
+ connection.public_send(*args) do |request|
79
+ request.headers['Authorization'] = config.token unless config.token.nil?
80
+ request.headers['Accept'] = 'application/vnd.duse.1+json'
81
+ end
82
+ end
83
+
84
+ def error_msg(json)
85
+ result = JSON.parse(json)['message']
86
+ result = result.join "\n" if result.is_a? Array
87
+ result
88
+ rescue
89
+ json
90
+ end
91
+
92
+ def connection
93
+ fail ArgumentError, 'Uri must be set' if config.uri.nil?
94
+
95
+ @connection ||= Faraday.new url: config.uri do |faraday|
96
+ faraday.request :json
97
+ faraday.response :json, content_type: /\bjson$/
98
+ faraday.adapter *faraday_adapter
99
+ end
100
+ end
101
+
102
+ def faraday_adapter
103
+ Faraday.default_adapter
104
+ end
105
+
106
+ private
107
+
108
+ def instances_from(entity, array)
109
+ array.map { |e| instance_from(entity, e) }
110
+ end
111
+
112
+ def instance_from(entity, hash)
113
+ instance = entity.new
114
+ entity.attributes.each do |attribute|
115
+ if hash.has_key? attribute
116
+ value = hash[attribute]
117
+ if Entity::MAP.has_key? attribute
118
+ nested_entity = Entity::MAP[attribute]
119
+ value = hash[attribute].map { |e| instance_from(nested_entity, e) }
120
+ end
121
+ instance.public_send("#{attribute}=", value)
122
+ end
123
+ end
124
+ instance
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,23 @@
1
+ require 'duse/client/entity'
2
+ require 'openssl'
3
+
4
+ module Duse
5
+ module Client
6
+ class User < Entity
7
+ attributes :id, :username, :email, :public_key, :password
8
+
9
+ id_field :id
10
+ one :user
11
+ many :users
12
+
13
+ def public_key
14
+ OpenSSL::PKey::RSA.new load_attribute 'public_key'
15
+ end
16
+
17
+ def public_key=(public_key)
18
+ public_key = public_key.to_s if public_key.is_a? OpenSSL::PKey::RSA
19
+ set_attribute('public_key', public_key)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Duse
5
+ module Encryption
6
+ module_function
7
+
8
+ def encrypt(private_key, public_key, text)
9
+ encrypted = public_key.public_encrypt text.force_encoding('ascii-8bit')
10
+ signature = sign(private_key, encrypted)
11
+ [encode(encrypted), signature]
12
+ end
13
+
14
+ def sign(private_key, text)
15
+ encode(private_key.sign(digest, text))
16
+ end
17
+
18
+ def decrypt(private_key, text)
19
+ private_key.private_decrypt(decode(text)).force_encoding('utf-8')
20
+ end
21
+
22
+ def verify(public_key, signature, encrypted)
23
+ public_key.verify digest, decode(signature), decode(encrypted)
24
+ end
25
+
26
+ def digest
27
+ OpenSSL::Digest::SHA256.new
28
+ end
29
+
30
+ def encode(plain_text)
31
+ Base64.encode64(plain_text).encode('utf-8')
32
+ end
33
+
34
+ def decode(encoded_text)
35
+ Base64.decode64(encoded_text.encode('ascii-8bit')).force_encoding('utf-8')
36
+ end
37
+ end
38
+ end
39
+