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.
- checksums.yaml +15 -0
- data/.gitignore +35 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +13 -0
- data/LICENSE +22 -0
- data/README.md +89 -0
- data/Rakefile +11 -0
- data/bin/duse +8 -0
- data/duse.gemspec +22 -0
- data/lib/duse.rb +10 -0
- data/lib/duse/cli.rb +104 -0
- data/lib/duse/cli/account.rb +20 -0
- data/lib/duse/cli/account_confirm.rb +22 -0
- data/lib/duse/cli/account_info.rb +18 -0
- data/lib/duse/cli/account_password.rb +14 -0
- data/lib/duse/cli/account_password_change.rb +30 -0
- data/lib/duse/cli/account_password_reset.rb +20 -0
- data/lib/duse/cli/account_resend_confirmation.rb +20 -0
- data/lib/duse/cli/account_update.rb +25 -0
- data/lib/duse/cli/api_command.rb +33 -0
- data/lib/duse/cli/cli_config.rb +54 -0
- data/lib/duse/cli/command.rb +202 -0
- data/lib/duse/cli/config.rb +16 -0
- data/lib/duse/cli/help.rb +23 -0
- data/lib/duse/cli/key_helper.rb +84 -0
- data/lib/duse/cli/login.rb +27 -0
- data/lib/duse/cli/meta_command.rb +12 -0
- data/lib/duse/cli/parser.rb +43 -0
- data/lib/duse/cli/password_helper.rb +17 -0
- data/lib/duse/cli/register.rb +45 -0
- data/lib/duse/cli/secret.rb +19 -0
- data/lib/duse/cli/secret_add.rb +38 -0
- data/lib/duse/cli/secret_generator.rb +10 -0
- data/lib/duse/cli/secret_get.rb +40 -0
- data/lib/duse/cli/secret_list.rb +20 -0
- data/lib/duse/cli/secret_remove.rb +19 -0
- data/lib/duse/cli/secret_update.rb +44 -0
- data/lib/duse/cli/share_with_user.rb +53 -0
- data/lib/duse/cli/version.rb +12 -0
- data/lib/duse/client/config.rb +36 -0
- data/lib/duse/client/entity.rb +87 -0
- data/lib/duse/client/namespace.rb +112 -0
- data/lib/duse/client/secret.rb +69 -0
- data/lib/duse/client/session.rb +128 -0
- data/lib/duse/client/user.rb +23 -0
- data/lib/duse/encryption.rb +39 -0
- data/lib/duse/version.rb +3 -0
- data/spec/cli/cli_config_spec.rb +49 -0
- data/spec/cli/commands/account_spec.rb +45 -0
- data/spec/cli/commands/config_spec.rb +17 -0
- data/spec/cli/commands/login_spec.rb +51 -0
- data/spec/cli/commands/register_spec.rb +38 -0
- data/spec/cli/commands/secret_spec.rb +142 -0
- data/spec/client/secret_marshaller_spec.rb +32 -0
- data/spec/client/secret_spec.rb +96 -0
- data/spec/client/user_spec.rb +105 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/support/helpers.rb +43 -0
- data/spec/support/mock_api.rb +142 -0
- 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
|
+
|