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