reedb 0.10.rc1
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 +7 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +38 -0
- data/Rakefile +2 -0
- data/bin/reedbd +62 -0
- data/lib/reedb.rb +739 -0
- data/lib/reedb/config.rb +24 -0
- data/lib/reedb/constants.rb +42 -0
- data/lib/reedb/daemon_wrapper.rb +617 -0
- data/lib/reedb/datafile.rb +268 -0
- data/lib/reedb/errors/daemon_errors.rb +34 -0
- data/lib/reedb/errors/encryption_errors.rb +29 -0
- data/lib/reedb/errors/vault_errors.rb +39 -0
- data/lib/reedb/reevault.rb +547 -0
- data/lib/reedb/security/aes.rb +94 -0
- data/lib/reedb/security/encryption.rb +64 -0
- data/lib/reedb/security/multifish.rb +15 -0
- data/lib/reedb/security/secure_hash.rb +131 -0
- data/lib/reedb/security/twofish.rb +14 -0
- data/lib/reedb/utils/logger.rb +97 -0
- data/lib/reedb/utils/meta_vault.rb +28 -0
- data/lib/reedb/utils/sorting.rb +49 -0
- data/lib/reedb/utils/utilities.rb +121 -0
- data/lib/reedb/utils/uuids.rb +375 -0
- data/lib/reedb/utils/version.rb +101 -0
- data/reedb.gemspec +31 -0
- data/tests/daemons/reedb.rb +27 -0
- data/tests/layout.rb +24 -0
- data/tests/networking/client.rb +14 -0
- data/tests/networking/server.rb +14 -0
- data/tests/play.rb +5 -0
- data/tests/serialisation.rb +49 -0
- data/tests/tests.rb +64 -0
- metadata +209 -0
@@ -0,0 +1,268 @@
|
|
1
|
+
# ====================================================
|
2
|
+
# Copyright 2015 Lonely Robot (see @author)
|
3
|
+
# @author: Katharina Sabel | www.2rsoftworks.de
|
4
|
+
#
|
5
|
+
# Distributed under the GNU Lesser GPL Version 3
|
6
|
+
# (See accompanying LICENSE file or get a copy at
|
7
|
+
# https://www.gnu.org/licenses/lgpl.html)
|
8
|
+
# ====================================================
|
9
|
+
|
10
|
+
require_relative 'utils/version'
|
11
|
+
require_relative 'security/encryption'
|
12
|
+
|
13
|
+
module Reedb
|
14
|
+
class DataFile
|
15
|
+
|
16
|
+
# Name of the datafile in clear.
|
17
|
+
# To get the hashed variant use the path getter.
|
18
|
+
#
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
# Path and hash of the file on the system. Used when caching files in a vault
|
22
|
+
#
|
23
|
+
attr_reader :path
|
24
|
+
|
25
|
+
# The latest version of this data file. Contains a version numeral and
|
26
|
+
# a last-changed timestamp to provide better history for
|
27
|
+
# multi-computer setups.
|
28
|
+
#
|
29
|
+
attr_reader :version
|
30
|
+
|
31
|
+
# attr_reader :dataset
|
32
|
+
|
33
|
+
# Default constructor for a datafile.
|
34
|
+
#
|
35
|
+
def initialize(name, vault, old_file = nil)
|
36
|
+
if old_file
|
37
|
+
@dataset = old_file
|
38
|
+
@name = @dataset['header']['name']
|
39
|
+
@version = Version1.new(@dataset['header']['latest'])
|
40
|
+
@vault = vault
|
41
|
+
else
|
42
|
+
@version = Version1.new
|
43
|
+
@vault = vault
|
44
|
+
@name = name
|
45
|
+
fill_file
|
46
|
+
end
|
47
|
+
|
48
|
+
# The header set is defined by the vault and specifies the fields that can
|
49
|
+
# be specified in the header part of a file.
|
50
|
+
construct_path(@name, @vault)
|
51
|
+
end
|
52
|
+
|
53
|
+
# New and improved data insetion function!
|
54
|
+
# Data needs to have a certain format
|
55
|
+
# {
|
56
|
+
# 'header': {
|
57
|
+
# ...
|
58
|
+
# },
|
59
|
+
# 'body' {
|
60
|
+
# ...
|
61
|
+
# }
|
62
|
+
# }
|
63
|
+
#
|
64
|
+
# Malformed data will be ignored and reported in the vault logging.
|
65
|
+
#
|
66
|
+
# Writing into body is farely straight forward. A value needs a field and is
|
67
|
+
# assigned a version automatically depending on the time in the cache cycle
|
68
|
+
# of the file
|
69
|
+
#
|
70
|
+
# Writing to header is much more interesting.
|
71
|
+
# There is a header_set defined in the parent vault that specifies what fields
|
72
|
+
# are valid and what types they allow. For example, by default, there is a 'urls'
|
73
|
+
# field that is a list of urls.
|
74
|
+
#
|
75
|
+
# WRITING THE SAME VALUE INTO A FIELD AS EXISTS BEFORE WILL RESET THAT VALUE!
|
76
|
+
# That's how you can delete a field with this same function in the same step as
|
77
|
+
# adding new information (I know...hacky. But clever)
|
78
|
+
#
|
79
|
+
# For further documentation on this function, please check with the Reedb wiki
|
80
|
+
#
|
81
|
+
# Params:
|
82
|
+
# data => specifically formatted JSON data set (see docs)
|
83
|
+
# mode => overwrite default file/ vault caching mode.
|
84
|
+
#
|
85
|
+
def insertv2(data, mode = :hard)
|
86
|
+
# Add option for :fast sync mode here at some point
|
87
|
+
sync if mode == :hard
|
88
|
+
|
89
|
+
# First split up the data and check what's actually there
|
90
|
+
to_head = if data['header'] then data['header'] else false end
|
91
|
+
to_body = if data['body'] then data['body'] else false end
|
92
|
+
|
93
|
+
# Returns an error code for malformed data
|
94
|
+
return FILE_MALFORMED_DATA_ERROR unless to_body || to_head
|
95
|
+
|
96
|
+
# Gets the time difference between edits.
|
97
|
+
# If it's not big enough (5 seconds) will not increment version number
|
98
|
+
current_time = DateTime.now.strftime('%Q').to_i
|
99
|
+
old_time = @version.timestamp.to_i
|
100
|
+
version_needs_changing = (current_time - old_time >= FILE_CACHE_TIME) && to_body
|
101
|
+
|
102
|
+
# Actually updates the version if neccessary
|
103
|
+
@version.update if version_needs_changing
|
104
|
+
|
105
|
+
# Now make sure the version branch actually exists
|
106
|
+
@dataset['body']["#{@version}"] = {} unless @dataset['body']["#{@version}"]
|
107
|
+
|
108
|
+
# Update the latest version pointer in the dataset
|
109
|
+
@dataset['header']['latest'] = "#{@version}" if version_needs_changing
|
110
|
+
|
111
|
+
if to_head
|
112
|
+
to_head.each do |key, value|
|
113
|
+
@dataset['header']['name'] if key == 'name'
|
114
|
+
|
115
|
+
# This means that the value can be stored
|
116
|
+
if @vault.header_set.include?(key)
|
117
|
+
|
118
|
+
# If inserting into a header single field
|
119
|
+
if @vault.header_set[key] == 'single'
|
120
|
+
if @dataset['header'][key]
|
121
|
+
@dataset['header'][key] = nil
|
122
|
+
else
|
123
|
+
@dataset['header'][key] = value
|
124
|
+
end
|
125
|
+
|
126
|
+
# If inserting into a header list
|
127
|
+
elsif @vault.header_set[key] == 'list'
|
128
|
+
if value.instance_of? String
|
129
|
+
if @dataset['header'][key].include?(value)
|
130
|
+
@dataset['header'][key].delete(value)
|
131
|
+
else
|
132
|
+
@dataset['header'][key] << value
|
133
|
+
end
|
134
|
+
elsif value.instance_of? Array
|
135
|
+
value.each do |sub_value|
|
136
|
+
if @dataset['header'][key].include?(sub_value)
|
137
|
+
@dataset['header'][key].delete(sub_value)
|
138
|
+
else
|
139
|
+
@dataset['header'][key] << sub_value
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# If inserting into a header tree
|
145
|
+
elsif @vault.header_set[key] == 'tree'
|
146
|
+
if @dataset['header'][key].include?(value)
|
147
|
+
@dataset['header'][key].delete(value)
|
148
|
+
else
|
149
|
+
@dataset['header'][key][value[0]] = value[1]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
else
|
153
|
+
# Report the incident
|
154
|
+
VaultLogger.write("Tried to write invalid data to #{@name}", 'warn')
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# BOOORING! :C
|
160
|
+
if to_body
|
161
|
+
to_body.each do |field, value|
|
162
|
+
@dataset['body']["#{@version}"]["#{field}"] = value
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
return self
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
#
|
171
|
+
#
|
172
|
+
|
173
|
+
def insert(data, mode = :hard)
|
174
|
+
puts "[LEGACY MODE]: This function has been depreciated. Use 'insertv2' instead!"
|
175
|
+
VaultLogger.write("[LEGACY MODE]: Using broken function. Aborting action!", 'error')
|
176
|
+
return
|
177
|
+
|
178
|
+
# => Updates the version of the file if neccesary
|
179
|
+
# puts (@dataset['body'][@version] == {})
|
180
|
+
|
181
|
+
# puts @dataset['body'][@version]
|
182
|
+
curr_time = DateTime.now.strftime('%Q').to_i
|
183
|
+
then_time = @version.timestamp
|
184
|
+
# time_diff = @version.timestamp - curr_time
|
185
|
+
time_dif = curr_time - then_time
|
186
|
+
|
187
|
+
unless @dataset['body'][@version] == {}
|
188
|
+
unless time_dif < 7500
|
189
|
+
@version.update
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
data.each do |master, section|
|
194
|
+
if master == 'body'
|
195
|
+
@dataset[master]["#{@version}"] = {} unless @dataset[master]["#{@version}"]
|
196
|
+
end
|
197
|
+
|
198
|
+
section.each do |key, value|
|
199
|
+
if master == 'header'
|
200
|
+
@dataset[master][key] = value
|
201
|
+
next
|
202
|
+
end
|
203
|
+
|
204
|
+
@dataset[master]["#{@version}"][key] = value
|
205
|
+
@dataset['header']['latest'] = @version
|
206
|
+
end
|
207
|
+
end
|
208
|
+
sync if mode == :hard
|
209
|
+
end
|
210
|
+
|
211
|
+
def cache mode
|
212
|
+
return @dataset['header'] if mode == :header
|
213
|
+
return @dataset if mode == :full
|
214
|
+
end
|
215
|
+
|
216
|
+
# Has two modes. In soft-mode it schedules a sync with the file system.
|
217
|
+
# In hard-mode (default) it force syncs itself with the filesystem by itself after every insert.
|
218
|
+
#
|
219
|
+
#
|
220
|
+
#
|
221
|
+
def sync(mode = :hard)
|
222
|
+
json_data = @dataset.to_json
|
223
|
+
# puts json_data
|
224
|
+
crypt_json = @vault.crypt.encrypt(json_data)
|
225
|
+
|
226
|
+
tmp = File.open("#{@path}", "wb+")
|
227
|
+
tmp.write(Base64.encode64("#{crypt_json}"))
|
228
|
+
tmp.close
|
229
|
+
VaultLogger.write("File #{@name} has been synced", 'debug')
|
230
|
+
return self
|
231
|
+
end
|
232
|
+
|
233
|
+
# Closes the file on the file system and dumps the header
|
234
|
+
# reference from the containing vault.
|
235
|
+
# Usually only called on vault close & lock.
|
236
|
+
#
|
237
|
+
def close
|
238
|
+
sync
|
239
|
+
remove_instance_variable(:@dataset)
|
240
|
+
VaultLogger.write("Closing file #{name}", 'debug')
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
def fill_file
|
246
|
+
@dataset = {}
|
247
|
+
@dataset['header'] = {}
|
248
|
+
@dataset['header']['name'] = @name
|
249
|
+
@dataset['header']['latest'] = @version
|
250
|
+
|
251
|
+
# Adds the header fields that were specified by the parent vault here
|
252
|
+
@vault.header_set.each do |field, type|
|
253
|
+
@dataset['header']["#{field}"] = nil if type == 'single'
|
254
|
+
@dataset['header']["#{field}"] = [] if type == 'list'
|
255
|
+
@dataset['header']["#{field}"] = {} if type == 'tree'
|
256
|
+
end
|
257
|
+
|
258
|
+
@dataset['body'] = {}
|
259
|
+
# @version.update
|
260
|
+
@dataset['body']["#{@version}"] = {}
|
261
|
+
# @dataset['body']["#{@version}"]['name'] = @name
|
262
|
+
end
|
263
|
+
|
264
|
+
def construct_path(name, vault)
|
265
|
+
@path = "#{vault.path}/data/#{SecurityUtils::tiger_hash(@name)}.ree"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# ====================================================
|
2
|
+
# Copyright 2015 Random Robot Softworks (see @author)
|
3
|
+
# @author: Katharina Sabel | www.2rsoftworks.de
|
4
|
+
#
|
5
|
+
# Distributed under the GNU Lesser GPL Version 3
|
6
|
+
# (See accompanying LICENSE file or get a copy at
|
7
|
+
# https://www.gnu.org/licenses/lgpl.html)
|
8
|
+
# ====================================================
|
9
|
+
|
10
|
+
# These have to do with tokens being old, bad or missing.
|
11
|
+
|
12
|
+
class DaemonError < StandardError
|
13
|
+
end
|
14
|
+
|
15
|
+
class VaultAlreadyScopedError < DaemonError
|
16
|
+
end
|
17
|
+
|
18
|
+
class VaultNotScopedError < DaemonError
|
19
|
+
end
|
20
|
+
|
21
|
+
class VaultNotAvailableError < DaemonError
|
22
|
+
end
|
23
|
+
|
24
|
+
class UnknownTokenError < DaemonError
|
25
|
+
end
|
26
|
+
|
27
|
+
class UnautherisedTokenError < DaemonError
|
28
|
+
end
|
29
|
+
|
30
|
+
class MissingTokenError < DaemonError
|
31
|
+
end
|
32
|
+
|
33
|
+
class FunctionNotImplementedError < DaemonError
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# ====================================================
|
2
|
+
# Copyright 2015 Random Robot Softworks (see @author)
|
3
|
+
# @author: Katharina Sabel | www.2rsoftworks.de
|
4
|
+
#
|
5
|
+
# Distributed under the GNU Lesser GPL Version 3
|
6
|
+
# (See accompanying LICENSE file or get a copy at
|
7
|
+
# https://www.gnu.org/licenses/lgpl.html)
|
8
|
+
# ====================================================
|
9
|
+
|
10
|
+
class EncryptionError < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
class MissingEncryptionTypeError < EncryptionError
|
14
|
+
end
|
15
|
+
|
16
|
+
class MissingUserPasswordError < EncryptionError
|
17
|
+
end
|
18
|
+
|
19
|
+
class WrongUserPasswordError < EncryptionError
|
20
|
+
end
|
21
|
+
|
22
|
+
class InsecureUserPasswordError < EncryptionError
|
23
|
+
end
|
24
|
+
|
25
|
+
class EncryptionFailedError < EncryptionError
|
26
|
+
end
|
27
|
+
|
28
|
+
class DecryptionFailedError < EncryptionError
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# ====================================================
|
2
|
+
# Copyright 2015 Random Robot Softworks (see @author)
|
3
|
+
# @author: Katharina Sabel | www.2rsoftworks.de
|
4
|
+
#
|
5
|
+
# Distributed under the GNU Lesser GPL Version 3
|
6
|
+
# (See accompanying LICENSE file or get a copy at
|
7
|
+
# https://www.gnu.org/licenses/lgpl.html)
|
8
|
+
# ====================================================
|
9
|
+
|
10
|
+
class VaultError < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
class VaultExistsAtLocationError < VaultError
|
14
|
+
end
|
15
|
+
|
16
|
+
class VaultDoesNotExistError < VaultError
|
17
|
+
end
|
18
|
+
|
19
|
+
class VaultWritePermissionsError < VaultError
|
20
|
+
end
|
21
|
+
|
22
|
+
class VaultMissingConfigurationError < VaultError
|
23
|
+
end
|
24
|
+
|
25
|
+
class VaultLoggerError < VaultError
|
26
|
+
end
|
27
|
+
|
28
|
+
# This has been depreciated.
|
29
|
+
class BadCacheError < VaultError
|
30
|
+
end
|
31
|
+
|
32
|
+
class FileNotFoundError < VaultError
|
33
|
+
end
|
34
|
+
|
35
|
+
class FileBusyError < VaultError
|
36
|
+
end
|
37
|
+
|
38
|
+
class MalformedSearchError < VaultError
|
39
|
+
end
|
@@ -0,0 +1,547 @@
|
|
1
|
+
# ====================================================
|
2
|
+
# Copyright 2015 Lonely Robot (see @author)
|
3
|
+
# @author: Katharina Sabel | www.2rsoftworks.de
|
4
|
+
#
|
5
|
+
# Distributed under the GNU Lesser GPL Version 3
|
6
|
+
# (See accompanying LICENSE file or get a copy at
|
7
|
+
# https://www.gnu.org/licenses/lgpl.html)
|
8
|
+
# ====================================================
|
9
|
+
|
10
|
+
# => Vault internals
|
11
|
+
require_relative 'datafile'
|
12
|
+
|
13
|
+
# => Import custom errors
|
14
|
+
require_relative 'errors/encryption_errors'
|
15
|
+
require_relative 'errors/vault_errors'
|
16
|
+
|
17
|
+
# => Import utilities and tools
|
18
|
+
require_relative 'utils/utilities'
|
19
|
+
require_relative 'utils/sorting'
|
20
|
+
require_relative 'utils/logger'
|
21
|
+
|
22
|
+
# => Import security package classes
|
23
|
+
require_relative 'security/multifish'
|
24
|
+
require_relative 'security/twofish'
|
25
|
+
require_relative 'security/aes'
|
26
|
+
|
27
|
+
# => Import internals
|
28
|
+
require 'fileutils'
|
29
|
+
require 'socket'
|
30
|
+
require 'json'
|
31
|
+
require 'yaml'
|
32
|
+
require 'etc'
|
33
|
+
|
34
|
+
module Reedb
|
35
|
+
class ReeVault
|
36
|
+
|
37
|
+
attr_reader :path
|
38
|
+
|
39
|
+
# Encryption handler to be used by the vault files
|
40
|
+
#
|
41
|
+
attr_reader :crypt
|
42
|
+
|
43
|
+
# Indicates the size of the vault as per data file entries.
|
44
|
+
# Is updated with every header cache and write cycle.
|
45
|
+
#
|
46
|
+
attr_reader :size
|
47
|
+
|
48
|
+
# Holds a hash of possible values that header files in this vault
|
49
|
+
# can have. Fields need to be specified by name and a type. To choose
|
50
|
+
# from 'single', 'list' and 'tree' (var, list, dict)
|
51
|
+
#
|
52
|
+
attr_reader :header_set
|
53
|
+
|
54
|
+
# Constructor for a vault with name, path and encryption enum.
|
55
|
+
# Valid encryption parameters are :aes, :twofish, :multi and :auto_fill
|
56
|
+
#
|
57
|
+
def initialize(name, path, encprytion)
|
58
|
+
@already_logging = false
|
59
|
+
|
60
|
+
# Header maps
|
61
|
+
@headers = {}
|
62
|
+
@hgroups = {}
|
63
|
+
|
64
|
+
# Fileset
|
65
|
+
@locked = false
|
66
|
+
@locks = []
|
67
|
+
|
68
|
+
# Defines the default (and boring vanilla) header set
|
69
|
+
@header_set = {
|
70
|
+
'urls'=>'list',
|
71
|
+
'tags'=>'list'
|
72
|
+
}
|
73
|
+
|
74
|
+
construct_path("#{name}", "#{path}")
|
75
|
+
init_encryption(encprytion) # => Throws exceptions!
|
76
|
+
self.secure_config(false)
|
77
|
+
return self
|
78
|
+
end
|
79
|
+
|
80
|
+
def secure_config(boolean = true)
|
81
|
+
@secure_config = boolean
|
82
|
+
return self
|
83
|
+
end
|
84
|
+
|
85
|
+
# Fields can be added to a vault header BUT NOT REMOVED AGAIN!
|
86
|
+
# So be careful what you put in your header.
|
87
|
+
# (aka upgrade yes, downgrade noooo)
|
88
|
+
#
|
89
|
+
# Unused fields can remain blank but need to stay in a vaults header list
|
90
|
+
# for backwards compatiblity
|
91
|
+
#
|
92
|
+
def add_header_field(name, type)
|
93
|
+
@header_set[name] = type unless @header_set[name]
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# Pokes if a vault exists
|
98
|
+
def try?
|
99
|
+
return self.includes?('config')
|
100
|
+
end
|
101
|
+
|
102
|
+
# Counts the vault contents and returns an Integer
|
103
|
+
# WITHOUT HAVING TO UNLOCK THE VAULT!
|
104
|
+
#
|
105
|
+
def count
|
106
|
+
counter = 0
|
107
|
+
Dir.glob("#{@path}/data/*.ree") do |f|
|
108
|
+
counter += 1
|
109
|
+
end
|
110
|
+
return counter
|
111
|
+
end
|
112
|
+
|
113
|
+
# Little helper method to determine if a vault is in the middle of
|
114
|
+
# a write cycle. Which would cause horrible crashes on other applications
|
115
|
+
# and errors on the file system if things are moved around inside
|
116
|
+
#
|
117
|
+
def locked?() return @locked end
|
118
|
+
|
119
|
+
def create(password = :failed)
|
120
|
+
# If keygen was used to set a user password then fetch it
|
121
|
+
# and remove the variable from memory!
|
122
|
+
return nil unless password?(password)
|
123
|
+
return nil unless encryption?(password)
|
124
|
+
|
125
|
+
# puts "This is the password: #{password}"
|
126
|
+
|
127
|
+
# => Encryption now active and key available under @crypt.key
|
128
|
+
@conf_path = "#{@path}/config"
|
129
|
+
|
130
|
+
needs_creation = true
|
131
|
+
|
132
|
+
if self.includes?('config')
|
133
|
+
raise VaultExistsAtLocationError.new, "Vault already exists at location #{@path}. Aborting operation..."
|
134
|
+
|
135
|
+
# => This rules out lots of code to be run
|
136
|
+
needs_creation = false
|
137
|
+
else
|
138
|
+
if Reedb::archos == :linux || Reedb::archos == :osx
|
139
|
+
FileUtils::mkdir_p(File.expand_path("#{@path}/data")) # => Data dir
|
140
|
+
FileUtils::mkdir(File.expand_path("#{@path}/shasums")) # => Checksum dir
|
141
|
+
FileUtils::mkdir(File.expand_path("#{@path}/logs")) # => Logs dir
|
142
|
+
|
143
|
+
FileUtils::chmod_R(0744, "#{@path}")
|
144
|
+
|
145
|
+
# This is used for windows because windows fucking sucks!
|
146
|
+
else
|
147
|
+
FileUtils::mkdir_p(File.expand_path("#{@path}/data")) # => Data dir
|
148
|
+
FileUtils::mkdir(File.expand_path("#{@path}/shasums")) # => Checksum dir
|
149
|
+
FileUtils::mkdir(File.expand_path("#{@path}/logs")) # => Logs dir
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Now that the vault directory exists logs can be opened.
|
154
|
+
init_logger(true)
|
155
|
+
|
156
|
+
if needs_creation
|
157
|
+
# Code that will only be run if the vault was just created on the system
|
158
|
+
time = Reedb::Utilities.get_time(false)
|
159
|
+
VaultLogger.write("Vault created on #{time}. All directories created successfully!")
|
160
|
+
|
161
|
+
# => Now creating configuration file
|
162
|
+
@config = {}
|
163
|
+
@config['vault_name'] = "#{@name}"
|
164
|
+
@config['creation_date'] = "#{Utilities.get_time}"
|
165
|
+
@config['last_updated'] = "#{Utilities.get_time}"
|
166
|
+
@config['creation_machine'] = "#{Socket.gethostname}"
|
167
|
+
@config['updating_machine'] = "#{Socket.gethostname}"
|
168
|
+
@config['creation_user'] = "#{Etc.getlogin}"
|
169
|
+
@config['updating_user'] = "#{Etc.getlogin}"
|
170
|
+
@config['header_set'] = "#{@header_set}"
|
171
|
+
@config['creation_version'] = "#{Reedb::VERSION}"
|
172
|
+
save_config
|
173
|
+
|
174
|
+
# Now writing encrypted key to file with ASCII armour
|
175
|
+
update_secure_info("cey", @encrypted_key)
|
176
|
+
# remove_instance_variable(:@encrypted_key)
|
177
|
+
end
|
178
|
+
self.load("#{password}")
|
179
|
+
end
|
180
|
+
|
181
|
+
def load(password)
|
182
|
+
unless self.includes?('config')
|
183
|
+
raise VaultDoesNotExistError.new("Loading vault failed because it couldn't be found at the specified path!")
|
184
|
+
end
|
185
|
+
init_logger(false)
|
186
|
+
|
187
|
+
# Check if the config needs to be read via ASCII64 or YAML
|
188
|
+
if self.includes?('pom')
|
189
|
+
# Config is stored with ASCII Armour
|
190
|
+
@config = read_secure_info('config')
|
191
|
+
else
|
192
|
+
@config = YAML.load_file("#{@path}/config")
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
|
197
|
+
return nil unless unlock_vault("#{password}")
|
198
|
+
VaultLogger.write("Finished loading vault", 'debug')
|
199
|
+
cache_headers
|
200
|
+
|
201
|
+
return self
|
202
|
+
end
|
203
|
+
|
204
|
+
# Read a single file from the vault in secure mode
|
205
|
+
# Returns the entire file or only it's current set in hashes.
|
206
|
+
#
|
207
|
+
def read_file(name, history = false)
|
208
|
+
|
209
|
+
# Loads the file data into a local variable if it exists
|
210
|
+
file_data = load_file_data(name)
|
211
|
+
if file_data == nil
|
212
|
+
raise FileNotFoundError.new("#{name} could not be read: File not found!")
|
213
|
+
return VAULT_FILE_NOT_FOUND_ERROR # If the exception isn't handled correctly
|
214
|
+
else
|
215
|
+
# This code is executed if the file was found (thus data is in file_data)
|
216
|
+
compiled = {}
|
217
|
+
compiled['header'] = {}
|
218
|
+
|
219
|
+
# Removes the latest version from the header because it is insignificant.
|
220
|
+
file_data['header'].each do |key, value|
|
221
|
+
compiled['header']["#{key}"] = value unless key == "latest"
|
222
|
+
end
|
223
|
+
|
224
|
+
if history
|
225
|
+
compiled['body'] = file_data['body']
|
226
|
+
else
|
227
|
+
body_list = []
|
228
|
+
file_data['body'].each do |key, value|
|
229
|
+
body_list << key
|
230
|
+
end
|
231
|
+
|
232
|
+
compiled['body'] = {}
|
233
|
+
|
234
|
+
# Now sort the list of body versions
|
235
|
+
body_list.heapsort!
|
236
|
+
body_list.each do |version|
|
237
|
+
file_data['body']["#{version}"].each do |key, value|
|
238
|
+
compiled['body']["#{key}"] = value
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Then return that value
|
244
|
+
return compiled
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Check the file API or the wiki to learn how this function works.
|
249
|
+
# This function is also used to delete fields from header space.
|
250
|
+
#
|
251
|
+
def update(name, data)
|
252
|
+
cache_headers # Cache headers first to be sure
|
253
|
+
|
254
|
+
(raise FileBusyError.new, "File #{name} busy" ; return) if @locks.include?(name)
|
255
|
+
@locks << name
|
256
|
+
|
257
|
+
if @headers.key?(name)
|
258
|
+
# Creates file object from existing file object.
|
259
|
+
df = DataFile.new(name, self, load_file_data(name, :secure))
|
260
|
+
df.insertv2(data, :hard) # Default cache mode
|
261
|
+
else
|
262
|
+
df = DataFile.new(name, self)
|
263
|
+
df.insertv2(data, :hard) # Default cache mode
|
264
|
+
end
|
265
|
+
|
266
|
+
# => Make sure that everything is up to date.
|
267
|
+
cache_headers
|
268
|
+
@config['updating_user'] = "#{Etc.getlogin}"
|
269
|
+
@config['updating_machine'] = "#{Socket.gethostname}"
|
270
|
+
@config['last_updated'] = "#{Utilities.get_time}"
|
271
|
+
@config['last_version'] = "#{Reedb::VERSION}"
|
272
|
+
save_config
|
273
|
+
|
274
|
+
# Sync and close the file.
|
275
|
+
df.sync.close
|
276
|
+
|
277
|
+
# Unlocks the file again for other processes to edit.
|
278
|
+
@locks.delete(name)
|
279
|
+
end
|
280
|
+
|
281
|
+
def remove_file name
|
282
|
+
path_to_file = load_file_hash(name)
|
283
|
+
if path_to_file
|
284
|
+
FileUtils.rm(path_to_file)
|
285
|
+
puts "Successfullly removed file #{name}"
|
286
|
+
VaultLogger.write("Removed file #{name} from vault.", 'debug')
|
287
|
+
else
|
288
|
+
raise FileNotFoundError.new("#{name} could not be removed: File not found!")
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Returns headers according to a search queury
|
293
|
+
#
|
294
|
+
# { 'name' => '__name__',
|
295
|
+
# 'url' => '__url__',
|
296
|
+
# 'tags' => '__tags__',
|
297
|
+
# 'generic_field' => '__generic_information__'
|
298
|
+
# }
|
299
|
+
#
|
300
|
+
# 'tags=search engines, internet#urls=www.poodle.com'
|
301
|
+
#
|
302
|
+
def list_headers search
|
303
|
+
query = {}
|
304
|
+
cache_headers # => This fills @headers and @hfields
|
305
|
+
|
306
|
+
return @headers unless search
|
307
|
+
|
308
|
+
begin
|
309
|
+
splat = search.split('#')
|
310
|
+
splat.each do |target|
|
311
|
+
slice = target.split('=')
|
312
|
+
query["#{slice[0]}"] = slice[1..-1]
|
313
|
+
end
|
314
|
+
# puts query
|
315
|
+
rescue
|
316
|
+
raise MalformedSearchError.new, "Malformed search data"
|
317
|
+
end
|
318
|
+
|
319
|
+
log_query = {}
|
320
|
+
candidates = []
|
321
|
+
|
322
|
+
query.each do |cat, data|
|
323
|
+
data.each do |val|
|
324
|
+
log_query["#{cat}"] = @hgroups["#{cat}"]["#{val}"] if @hgroups["#{cat}"].include?(val)
|
325
|
+
log_query["#{cat}"].each { |c| candidates << c unless candidates.include?(c) }
|
326
|
+
end
|
327
|
+
end
|
328
|
+
return_buffer = candidates
|
329
|
+
candidates.each do |can|
|
330
|
+
log_query.each do |cat, data|
|
331
|
+
return_buffer.delete(can) unless log_query["#{cat}"].include?(can)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
return return_buffer
|
336
|
+
end
|
337
|
+
|
338
|
+
# Dump headers and files from memory in times of
|
339
|
+
# inactivity for security reasons
|
340
|
+
def unload
|
341
|
+
remove_instance_variable(:@headers)
|
342
|
+
@headers = {}
|
343
|
+
|
344
|
+
VaultLogger.write("It has been $TIME since the last interaction. Unloading vault contents for security reasons.", 'debug')
|
345
|
+
end
|
346
|
+
|
347
|
+
def close
|
348
|
+
VaultLogger.write("Scheduled closing of the vault.", 'debug')
|
349
|
+
@crypt.stop_encryption if @crypt.init
|
350
|
+
|
351
|
+
# Removing class variables for cleanup
|
352
|
+
remove_instance_variable(:@crypt)
|
353
|
+
remove_instance_variable(:@headers)
|
354
|
+
end
|
355
|
+
|
356
|
+
# Quickly returns if a file exists in the vault or it's children.
|
357
|
+
def includes? file
|
358
|
+
return File.exists?("#{@path}/#{file}")
|
359
|
+
end
|
360
|
+
|
361
|
+
def to_s
|
362
|
+
return "Vault: #{@name}, Path: #{@path}, File count: #{@headers.length}"
|
363
|
+
end
|
364
|
+
|
365
|
+
private
|
366
|
+
|
367
|
+
# Caches the current set of headers on a vault.
|
368
|
+
#
|
369
|
+
def cache_headers
|
370
|
+
@headers = {}
|
371
|
+
|
372
|
+
VaultLogger.write("Starting a cache cycle.", 'debug')
|
373
|
+
|
374
|
+
Dir.glob("#{@path}/data/*.ree") do |file|
|
375
|
+
f = File.open(file, 'r')
|
376
|
+
encrypted = Base64.decode64(f.read)
|
377
|
+
decrypted = @crypt.decrypt(encrypted)
|
378
|
+
|
379
|
+
data = JSON.parse(decrypted)
|
380
|
+
df = DataFile.new(nil, self, data)
|
381
|
+
|
382
|
+
tmp_head = df.cache(:header)
|
383
|
+
tmp_name = df.name
|
384
|
+
|
385
|
+
# Blank the df variable just in case.
|
386
|
+
df = 0x111111111111111
|
387
|
+
|
388
|
+
@headers[tmp_name] = tmp_head
|
389
|
+
|
390
|
+
# Now work with the header set to determine sub-groups
|
391
|
+
tmp_head.each do |category, data|
|
392
|
+
|
393
|
+
# This will loop through all the category groups in the
|
394
|
+
# header that have been registered
|
395
|
+
if @header_set.include?(category)
|
396
|
+
|
397
|
+
# Creates a new sub-hash for data
|
398
|
+
@hgroups["#{category}"] = {} unless @hgroups["#{category}"]
|
399
|
+
|
400
|
+
if @header_set["#{category}"] == 'list'
|
401
|
+
data.each do |target|
|
402
|
+
@hgroups["#{category}"]["#{target}"] = [] unless @hgroups["#{category}"]["#{target}"]
|
403
|
+
@hgroups["#{category}"]["#{target}"] << tmp_name unless @hgroups["#{category}"]["#{target}"].include?(tmp_name)
|
404
|
+
end
|
405
|
+
|
406
|
+
elsif @header_set["#{category}"] == 'single'
|
407
|
+
@hgroups["#{category}"] = "#{data}"
|
408
|
+
end
|
409
|
+
|
410
|
+
#TODO: Implement dictionary head later
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def load_file_hash name
|
417
|
+
cache_headers
|
418
|
+
if @headers.key?(name)
|
419
|
+
name_hash = SecurityUtils::tiger_hash("#{name}")
|
420
|
+
return "#{@path}/data/#{name_hash}.ree"
|
421
|
+
else
|
422
|
+
return false
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
# Loads a file with the clear name from headers.
|
427
|
+
# If file isn't found in headers vault is recached
|
428
|
+
# and file is then loaded from headers.
|
429
|
+
#
|
430
|
+
# If file isn't found in headers error is output.
|
431
|
+
#
|
432
|
+
def load_file_data(name, mode = :secure)
|
433
|
+
cache_headers
|
434
|
+
VaultLogger.write("Loading file #{name} from vault", 'debug')
|
435
|
+
if @headers.key?(name)
|
436
|
+
name_hash = SecurityUtils::tiger_hash("#{name}")
|
437
|
+
file_path = "#{@path}/data/#{name_hash}.ree"
|
438
|
+
f = File.open(file_path, 'r')
|
439
|
+
encrypted = Base64.decode64(f.read)
|
440
|
+
decrypted = @crypt.decrypt(encrypted)
|
441
|
+
|
442
|
+
return JSON.parse(decrypted) if mode == :secure
|
443
|
+
else
|
444
|
+
return nil
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
def save_config
|
449
|
+
@conf_path = "#{@path}/config"
|
450
|
+
if @secure_config
|
451
|
+
update_secure_info('config', @config)
|
452
|
+
par_path = "#{@path}/pom"
|
453
|
+
msg = "Why are you reading this?"
|
454
|
+
File.open("#{par_path}", "wb").write(Base64.encode64("#{msg}"))
|
455
|
+
else
|
456
|
+
File.open("#{@conf_path}", "wb+"){ |f| YAML.dump(@config, f) }
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Builds the vault path from a path, name and trimming
|
461
|
+
# additional slashes from the end.
|
462
|
+
#
|
463
|
+
def construct_path(name, path)
|
464
|
+
(@name = name ; @path = "")
|
465
|
+
path.end_with?("/") ? @path = "#{path}#{name}.reevault" : @path = "#{path}/#{name}.reevault"
|
466
|
+
end
|
467
|
+
|
468
|
+
def update_secure_info(name, data = nil)
|
469
|
+
path = "#{@path}/#{name}"
|
470
|
+
File.open(path, "wb").write(Base64.encode64("#{data}"))
|
471
|
+
end
|
472
|
+
|
473
|
+
def read_secure_info name
|
474
|
+
path = "#{@path}/#{name}"
|
475
|
+
return Base64.decode64(File.open(path, "r").read())
|
476
|
+
end
|
477
|
+
|
478
|
+
def init_logger bool
|
479
|
+
begin
|
480
|
+
unless logger?(bool)
|
481
|
+
raise VaultLoggerError.new, "Logger failed to be initialised"
|
482
|
+
end
|
483
|
+
rescue VaultError => e
|
484
|
+
puts e.message
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def logger? bool
|
489
|
+
(return false) if @already_logging && bool
|
490
|
+
|
491
|
+
VaultLogger.setup("#{@path}")
|
492
|
+
(@already_logging = true ; return true)
|
493
|
+
end
|
494
|
+
|
495
|
+
def password? password
|
496
|
+
raise MissingUserPasswordError.new, "Encryption error: Missing user password!" if password == nil
|
497
|
+
|
498
|
+
raise InsecureUserPasswordError.new, "Encryption error: Password too short! See: https://xkcd.com/936/" if password.length < Reedb::passlength
|
499
|
+
|
500
|
+
return true
|
501
|
+
end
|
502
|
+
|
503
|
+
def encryption? password
|
504
|
+
begin
|
505
|
+
@encrypted_key = @crypt.start_encryption(password)
|
506
|
+
rescue EncryptionError => e
|
507
|
+
puts e.message
|
508
|
+
return false
|
509
|
+
end
|
510
|
+
return true
|
511
|
+
end
|
512
|
+
|
513
|
+
# Unlocks the vault by decrypting the key and loading it into memory
|
514
|
+
# Enables the cryptographic module to decrypt and encrypt files.
|
515
|
+
#
|
516
|
+
def unlock_vault pw
|
517
|
+
begin
|
518
|
+
@encrypted_key = read_secure_info('cey') unless @encrypted_key
|
519
|
+
@crypt.start_encryption(pw, @encrypted_key)
|
520
|
+
remove_instance_variable(:@encrypted_key) if @encrypted_key
|
521
|
+
rescue #TODO: Check exception type here.
|
522
|
+
raise WrongUserPasswordError.new, "Incorrect user passphrase. Could not unlock!"
|
523
|
+
end
|
524
|
+
|
525
|
+
# Return values for the rest of the file.
|
526
|
+
return @crypt.init
|
527
|
+
end
|
528
|
+
|
529
|
+
# This method checks what encryption to use by enums.
|
530
|
+
# This can throw an exception if something was parsed incorrectly
|
531
|
+
# After this call the @crypt object has been initialised.
|
532
|
+
#
|
533
|
+
def init_encryption type
|
534
|
+
type = :aes if type == :auto
|
535
|
+
if type == :aes
|
536
|
+
@crypt = Reedb::RAES.new
|
537
|
+
elsif type == :twofish
|
538
|
+
@crypt = Reedb::Fish.new
|
539
|
+
elsif type == :multi
|
540
|
+
@crypt = Reedb::MLE.new
|
541
|
+
else
|
542
|
+
raise MissingEncryptionTypeError.new, "Encryption failed: Missing type. Aborting..."
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
end # class close
|
547
|
+
end # module close
|