donjon 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.
@@ -0,0 +1,59 @@
1
+ require 'donjon/encrypted_file'
2
+ require 'json'
3
+
4
+ module Donjon
5
+ class Database
6
+ def initialize(actor:)
7
+ @actor = actor
8
+ end
9
+
10
+ def [](key)
11
+ file = _file(key)
12
+ return unless file.readable?
13
+ _key, value = _unpack(file.read)
14
+ assert(key == _key, "bad stored data for #{key}!")
15
+ return value
16
+ end
17
+
18
+ def []=(key, value)
19
+ _file(key).write(_pack(key, value))
20
+ end
21
+
22
+ def each
23
+ parent = @actor.repo.join('data')
24
+ return unless parent.exist?
25
+ parent.children.each do |child|
26
+ path = "data/#{child.basename}"
27
+ file = EncryptedFile.new(path: path, actor: @actor)
28
+ next unless file.readable?
29
+ yield *_unpack(file.read)
30
+ end
31
+ end
32
+
33
+ def update
34
+ each do |key, value|
35
+ self[key] = value
36
+ end
37
+ nil
38
+ end
39
+
40
+
41
+ private
42
+
43
+ def _pack(key, value)
44
+ JSON.dump([key, value])
45
+ end
46
+
47
+ def _unpack(data)
48
+ JSON.parse(data)
49
+ end
50
+
51
+ def _hash(key)
52
+ OpenSSL::Digest::SHA256.hexdigest(key)
53
+ end
54
+
55
+ def _file(key)
56
+ EncryptedFile.new(path: "data/#{_hash(key)}", actor: @actor)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,87 @@
1
+ require 'openssl'
2
+ require 'gibberish'
3
+ require 'core_ext/assert'
4
+ require 'digest'
5
+
6
+ module Donjon
7
+ class EncryptedFile
8
+ def initialize(path:, actor:)
9
+ @path = path
10
+ @actor = actor
11
+ end
12
+
13
+ def exist?
14
+ _base_path.exist?
15
+ end
16
+
17
+ def readable?
18
+ _path_for(@actor).exist?
19
+ end
20
+
21
+ def read
22
+ path = _path_for(@actor)
23
+ exist? or raise 'file does not exist'
24
+ path.exist? or raise 'you were not granted access'
25
+ data = path.binread
26
+ _decrypt_from(@actor, data)
27
+ end
28
+
29
+ def write(data)
30
+ if data.nil?
31
+ _base_path.rmtree if exist?
32
+ return
33
+ end
34
+
35
+ User.each(@actor.repo) do |user|
36
+ payload = _encrypt_for(user, data)
37
+ path = _path_for(user)
38
+ path.parent.mkpath
39
+ path.binwrite(payload)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # random bytes added to the data to encrypt to obfuscate it
46
+ PADDING = 256
47
+
48
+ def _decrypt_from(user, data)
49
+ encrypted_key = data[0...256]
50
+ encrypted_data = data[256..-1]
51
+
52
+ # _log_key "before decrypt", encrypted_key
53
+ decrypted_pw = user.key.private_decrypt(encrypted_key)
54
+ # _log_key "decrypted", decrypted_pw
55
+
56
+ assert(decrypted_pw.size == 32)
57
+ payload = Gibberish::AES.new(decrypted_pw).decrypt(encrypted_data, binary: true)
58
+ payload[0...-PADDING]
59
+ end
60
+
61
+ def _encrypt_for(user, data)
62
+ payload = data + OpenSSL::Random.random_bytes(PADDING)
63
+ password = OpenSSL::Random.random_bytes(32)
64
+ encrypted_data = Gibberish::AES.new(password).encrypt(payload, binary: true)
65
+
66
+ # _log_key "before crypto", password
67
+ encrypted_key = user.key.public_encrypt(password)
68
+ # _log_key "encrypted", encrypted_key
69
+
70
+ assert(encrypted_key.size == 256)
71
+ encrypted_key + encrypted_data
72
+ end
73
+
74
+ def _log_key(message, key)
75
+ puts "#{message}: #{key.bytesize} bytes"
76
+ puts key.bytes.map { |b| "%02x" % b }.join(":")
77
+ end
78
+
79
+ def _base_path
80
+ @actor.repo.join(@path)
81
+ end
82
+
83
+ def _path_for(user)
84
+ _base_path.join("#{user.name}.db")
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,6 @@
1
+ require 'pathname'
2
+
3
+ module Donjon
4
+ class Repository < Pathname
5
+ end
6
+ end
@@ -0,0 +1,60 @@
1
+ require 'yaml'
2
+
3
+ module Donjon
4
+ class Settings
5
+ attr_reader :path
6
+
7
+ def initialize(path = nil)
8
+ @path = path || _default_path
9
+ @data = nil
10
+ end
11
+
12
+ def configured?
13
+ user_name && private_key && vault_path
14
+ end
15
+
16
+ def method_missing(method_name, *args, &block)
17
+ if method_name.to_s.end_with?('=')
18
+ set(method_name.to_s.chop, *args)
19
+ else
20
+ get(method_name.to_s, *args)
21
+ end
22
+ end
23
+
24
+ def respond_to?(method_name)
25
+ !!(method_name.to_s =~ /[a-z][a-z_]*=?/)
26
+ end
27
+
28
+ private
29
+
30
+ def get(key)
31
+ @data ||= _load
32
+ @data[key]
33
+ end
34
+
35
+ def set(key, value)
36
+ @data ||= _load
37
+ @data[key] = value
38
+ _save(@data)
39
+ value
40
+ end
41
+
42
+ def _load
43
+ @path.exist? ? YAML.load_file(@path) : {}
44
+ end
45
+
46
+ def _save(data)
47
+ @data['timestamp'] = Time.now
48
+ @path.parent.mkpath
49
+ @path.write data.to_yaml
50
+ end
51
+
52
+ def _fallback_path
53
+ Pathname.new('~').join('.donjonrc').expand_path
54
+ end
55
+
56
+ def _default_path
57
+ ENV.fetch('DONJONRC', _fallback_path)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,19 @@
1
+ require 'thor'
2
+ require 'delegate'
3
+ require 'singleton'
4
+
5
+ module Donjon
6
+ class Shell < SimpleDelegator
7
+ include Singleton
8
+
9
+ def initialize
10
+ shell = if $stdout.tty?
11
+ Thor::Shell::Color.new
12
+ else
13
+ Thor::Shell::Basic.new
14
+ end
15
+ super(shell)
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,61 @@
1
+ require 'yaml'
2
+ require 'core_ext/assert'
3
+
4
+ module Donjon
5
+ class User
6
+ attr_reader :name, :key, :repo
7
+
8
+ def initialize(name:, key:, repo:)
9
+ assert(key.n.num_bits == 2048)
10
+
11
+ @name = name
12
+ @key = key
13
+ @repo = repo
14
+ end
15
+
16
+ def save
17
+ _path_for(@name, @repo).tap do |path|
18
+ path.parent.mkpath
19
+ path.write @key.public_key.to_pem
20
+ end
21
+ self
22
+ end
23
+
24
+ private
25
+
26
+ module SharedMethods
27
+ private
28
+
29
+ def _path_for(name, repo)
30
+ repo.join("users/#{name}.pub")
31
+ end
32
+ end
33
+ extend SharedMethods
34
+ include SharedMethods
35
+
36
+ module ClassMethods
37
+ def find(name:, repo:)
38
+ path = _path_for(name, repo)
39
+ return unless path.exist?
40
+ key = OpenSSL::PKey::RSA.new(path.read)
41
+ new(name: name, key: key, repo: repo)
42
+ end
43
+
44
+ def each(repo, &block)
45
+ container = repo.join('users')
46
+ return unless container.exist?
47
+ container.children.each do |child|
48
+ next unless child.extname == '.pub'
49
+ name = child.basename.to_s.chomp('.pub')
50
+ key = OpenSSL::PKey::RSA.new(child.read)
51
+ block.call new(name: name, key: key, repo: repo)
52
+ end
53
+ end
54
+ end
55
+ extend ClassMethods
56
+ end
57
+
58
+
59
+
60
+ end
61
+
@@ -0,0 +1,3 @@
1
+ module Donjon
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/keys'
3
+ require 'spec/support/repos'
4
+ require 'donjon/database'
5
+ require 'donjon/user'
6
+
7
+ describe Donjon::Database do
8
+ let_repo :repo
9
+
10
+ let(:actor) {
11
+ Donjon::User.new(name: 'alice', key: random_key, repo: repo).save
12
+ }
13
+
14
+ let(:other_user) {
15
+ Donjon::User.new(name: 'bob', key: random_key, repo: repo).save
16
+ }
17
+
18
+ let(:options) {{
19
+ actor: actor
20
+ }}
21
+
22
+ subject { described_class.new(**options) }
23
+
24
+ describe '#initialize' do
25
+ it 'passes with valid options' do
26
+ expect { subject }.not_to raise_error
27
+ end
28
+
29
+ it 'requires :actor' do
30
+ options.delete :actor
31
+ expect { subject }.to raise_error
32
+ end
33
+ end
34
+
35
+ describe '#[]=' do
36
+ it 'passes' do
37
+ expect {
38
+ subject['foo'] = 'bar'
39
+ }.not_to raise_error
40
+ end
41
+ end
42
+
43
+ describe '#[]' do
44
+ it 'returns nil is nothing saved' do
45
+ expect( subject['foo1'] ).to be_nil
46
+ end
47
+
48
+ it 'returns nil if nil saved' do
49
+ subject['foo2'] = nil
50
+ expect( subject['foo2'] ).to be_nil
51
+ end
52
+
53
+ it 'returns previously save values' do
54
+ subject['foo3'] = 'bar3'
55
+ expect( subject['foo3'] ).to eq('bar3')
56
+ end
57
+
58
+ it 'returns nil when the key is not readable' do
59
+ subject['foo4'] = 'bar4'
60
+ other_db = described_class.new(actor: other_user)
61
+ expect(other_db['foo4']).to be_nil
62
+ end
63
+ end
64
+
65
+ describe '#update' do
66
+ it 'makes keys reable for other users' do
67
+ subject['foo'] = 'bar'
68
+ other_db = described_class.new(actor: other_user)
69
+ subject.update
70
+ expect(other_db['foo']).to eq('bar')
71
+ end
72
+ end
73
+
74
+ describe '#each' do
75
+ end
76
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+ require 'donjon/encrypted_file'
3
+ require 'donjon/user'
4
+ require 'spec/support/repos'
5
+ require 'spec/support/keys'
6
+
7
+ describe Donjon::EncryptedFile do
8
+ let_repo :repo
9
+
10
+ let(:actor) {
11
+ Donjon::User.new(key: random_key, name: 'alice', repo: repo)
12
+ }
13
+
14
+ let(:other_user) {
15
+ Donjon::User.new(key: random_key, name: 'bob', repo: repo)
16
+ }
17
+
18
+ let(:options) {{
19
+ path: 'foo', actor: actor
20
+ }}
21
+
22
+ subject { described_class.new(**options) }
23
+
24
+ describe '#initialize' do
25
+ it 'passes with valid options' do
26
+ expect { subject }.not_to raise_error
27
+ end
28
+
29
+ it 'requires :path' do
30
+ options.delete :path
31
+ expect { subject }.to raise_error
32
+ end
33
+
34
+ it 'requires :actor' do
35
+ options.delete :actor
36
+ expect { subject }.to raise_error
37
+ end
38
+ end
39
+
40
+ describe '#write' do
41
+ before { actor.save }
42
+
43
+ it 'passes' do
44
+ expect { subject.write('foo') }.not_to raise_error
45
+ end
46
+
47
+ it 'passes with large data' do
48
+ expect { subject.write('foo' * 1024) }.not_to raise_error
49
+ end
50
+
51
+ it 'works twice' do
52
+ expect {
53
+ 2.times { subject.write 'foo' }
54
+ }.not_to raise_error
55
+ end
56
+ end
57
+
58
+ describe '#read' do
59
+ before do
60
+ actor.save
61
+ other_user.save
62
+
63
+ described_class.
64
+ new(actor: actor, path: options[:path]).
65
+ write('hello, world!')
66
+ end
67
+
68
+
69
+ it 'returns decrypted contents' do
70
+ expect(subject.read).to eq('hello, world!')
71
+ end
72
+
73
+ it 'works for other users' do
74
+ data = described_class.
75
+ new(actor: other_user, path: options[:path]).
76
+ read
77
+ expect(data).to eq('hello, world!')
78
+ end
79
+ end
80
+
81
+ describe '#readable?' do
82
+ let(:other_file) {
83
+ described_class.new(actor: other_user, path: options[:path])
84
+ }
85
+
86
+ before { actor.save }
87
+
88
+ it 'is false for non-existing files' do
89
+ expect(subject).not_to be_readable
90
+ end
91
+
92
+ it 'is true for files I wrote' do
93
+ subject.write 'foo'
94
+ expect(subject).to be_readable
95
+ end
96
+
97
+ it 'is false for users added after I wrote' do
98
+ subject.write 'foo'
99
+ expect(other_file).not_to be_readable
100
+ end
101
+
102
+ it 'is true for users added before I wrote' do
103
+ other_user.save
104
+ subject.write 'foo'
105
+ expect(other_file).to be_readable
106
+ end
107
+ end
108
+ end
109
+
110
+