donjon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+