syde 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.
- data/.gemtest +0 -0
- data/Gemfile +3 -0
- data/LICENSE.mit +21 -0
- data/README.md +113 -0
- data/lib/syde/crypto.rb +38 -0
- data/lib/syde/errors.rb +7 -0
- data/lib/syde/storage.rb +37 -0
- data/lib/syde/vault.rb +214 -0
- data/lib/syde/vault.rb~ +214 -0
- data/lib/syde.rb +23 -0
- data/lib/syde.rb~ +23 -0
- data/rakefile +7 -0
- data/syde.gemspec +16 -0
- data/test/_test.rb +8 -0
- data/test/test_crypto.rb +30 -0
- data/test/test_storage.rb +90 -0
- data/test/test_vault.rb +68 -0
- metadata +86 -0
data/.gemtest
ADDED
File without changes
|
data/Gemfile
ADDED
data/LICENSE.mit
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
(MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2010 Adam Prescott
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
Syde
|
2
|
+
====
|
3
|
+
|
4
|
+
Syde is a *sy*mmetric *d*ata *e*ncryption library written in Ruby, licensed under the MIT license. It provides a saved encrypted data storage under a single password.
|
5
|
+
|
6
|
+
# How do I use this thing?
|
7
|
+
|
8
|
+
To install from RubyGems:
|
9
|
+
|
10
|
+
gem install syde
|
11
|
+
|
12
|
+
To get the source:
|
13
|
+
|
14
|
+
git clone https://github.com/aprescott/syde.git
|
15
|
+
|
16
|
+
To run the tests with the source:
|
17
|
+
|
18
|
+
rake test
|
19
|
+
|
20
|
+
To contribute:
|
21
|
+
|
22
|
+
* Fork it
|
23
|
+
* Make a new feature branch: `git checkout -b some-new-thing master`
|
24
|
+
* Pull request
|
25
|
+
|
26
|
+
Basic usage
|
27
|
+
-----------
|
28
|
+
|
29
|
+
require "syde"
|
30
|
+
|
31
|
+
vault = Syde::Vault.create("password")
|
32
|
+
vault #=> #<Vault (locked)>
|
33
|
+
vault.unlock!("password")
|
34
|
+
vault #=> #<Vault (unlocked)>
|
35
|
+
vault.contents #=> []
|
36
|
+
vault << "something important"
|
37
|
+
vault.contents #=> ["something important"]
|
38
|
+
vault.lock
|
39
|
+
vault #=> #<Vault (locked)>
|
40
|
+
|
41
|
+
### Reopening the vault
|
42
|
+
|
43
|
+
When you call Vault.create your vault is saved to disk, in a default location. To reopen the vault using the default location, you can use `Vault.open`. Adding contents to the vault will cause it to be saved automatically, without needing to `Vault#lock`.
|
44
|
+
|
45
|
+
### Auto-locking
|
46
|
+
|
47
|
+
By default, there is a 5-minute auto-lock timeout period. After 5 minutes has passed since unlocking the vault, the vault will automatically lock itself. It is possible to set the time manually:
|
48
|
+
|
49
|
+
vault.unlock("password", 2 * 60)
|
50
|
+
|
51
|
+
will set the timeout period to 2 minutes. Using a non-positive length of time will not unlock the vault. To unlock the vault indefinitely, use `unlock!`.
|
52
|
+
|
53
|
+
### Deleting contents of the vault
|
54
|
+
|
55
|
+
To delete something in the vault, use `delete`:
|
56
|
+
|
57
|
+
vault << "foo"
|
58
|
+
vault.contents
|
59
|
+
vault.delete("foo")
|
60
|
+
vault.contents
|
61
|
+
|
62
|
+
### Modifying contents in-place
|
63
|
+
|
64
|
+
vault.contents #=> ["foo"]
|
65
|
+
string = vault.contents.first #=> "foo"
|
66
|
+
string.replace("bar")
|
67
|
+
vault.contents #=> ["bar"]
|
68
|
+
|
69
|
+
Objects in the vault are serialised and then deserialised and as such are not modifiable.
|
70
|
+
|
71
|
+
### Available data
|
72
|
+
|
73
|
+
To see the data being stored in the vault file, use `data`. When the vault is unlocked, the contents are visible via `data`; otherwise, only the encrypted contents are visible.
|
74
|
+
|
75
|
+
### Specifying a different file
|
76
|
+
|
77
|
+
By default the file used for vault storage is ~/.syde/storage_file.vault. To change this, when calling `create`, pass a `String` as the second argument, for the filepath to be used.
|
78
|
+
|
79
|
+
Some details
|
80
|
+
------------
|
81
|
+
|
82
|
+
The password you give is used to encrypt, using the OpenSSL standard library, a random 4096-bit secret key generated from /dev/urandom. This secret key is then used for the encryption and decryption of the vault contents. When a vault is created it will by default have its information stored in ~/.syde/storage_file.vault. Vault contents, and the secret 4096-bit key are kept in this file in encrypted form only, and never written to the file as plaintext; when the vault is unlocked the plaintext is available to the running application.
|
83
|
+
|
84
|
+
Modifying the contents of the .vault file is not recommended, as it will cause Syde::Vault to be unable to open it.
|
85
|
+
|
86
|
+
### Vault contents
|
87
|
+
|
88
|
+
The contents of a vault are currently a simple Array, and are serialised with YAML before being encrypted. When the vault is opened and unlocked, the ciphertext is decrypted and then deserialised back to Ruby objects.
|
89
|
+
|
90
|
+
### Forgotten passwords
|
91
|
+
|
92
|
+
If your password is forgotten it's unlikely that you'll be able to retrieve any of the data stored in encrypted form as plaintext.
|
93
|
+
|
94
|
+
Tests
|
95
|
+
-----
|
96
|
+
|
97
|
+
`rake test` will run the set of unit tests.
|
98
|
+
|
99
|
+
Ruby versions
|
100
|
+
-------------
|
101
|
+
|
102
|
+
Should work without incident on 1.8.7.
|
103
|
+
|
104
|
+
TODO/issues
|
105
|
+
-----------
|
106
|
+
|
107
|
+
Fix encoding problems to get 1.9 support.
|
108
|
+
|
109
|
+
The intention is to have future versions support storing data associated to keys, allowing you to use
|
110
|
+
|
111
|
+
vault.add :password => "important"
|
112
|
+
|
113
|
+
This can be done with the current design by setting the contents of the vault to be just a hash.
|
data/lib/syde/crypto.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Syde
|
2
|
+
class Vault
|
3
|
+
module Crypto
|
4
|
+
def self.random_bytes(length)
|
5
|
+
File.open("/dev/urandom") { |f| f.read(length) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.new_iv
|
9
|
+
cipher.random_iv
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.aes(mode, key, iv, text)
|
13
|
+
c = cipher.send(mode)
|
14
|
+
c.key = digest(key)
|
15
|
+
c.iv = iv
|
16
|
+
c.update(text) << c.final
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.encrypt(key, iv, plaintext)
|
20
|
+
aes(:encrypt, key, iv, plaintext)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.decrypt(key, iv, ciphertext)
|
24
|
+
aes(:decrypt, key, iv, ciphertext)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.digest(input)
|
28
|
+
OpenSSL::Digest::SHA256.digest(input)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def self.cipher
|
34
|
+
OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/syde/errors.rb
ADDED
data/lib/syde/storage.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Syde
|
2
|
+
class Vault
|
3
|
+
module Storage
|
4
|
+
DefaultStorageFile = File.expand_path("~/.syde/storage_file.vault")
|
5
|
+
FileUtils.touch(DefaultStorageFile) unless File.exist?(DefaultStorageFile)
|
6
|
+
|
7
|
+
def self.valid_format?(data)
|
8
|
+
data.is_a?(Hash) &&
|
9
|
+
data.keys.include?(:encrypted) &&
|
10
|
+
data.keys.include?(:plaintext) &&
|
11
|
+
data[:plaintext].keys.include?(:iv) &&
|
12
|
+
data[:plaintext].keys.include?(:secret_key_hash) &&
|
13
|
+
data[:encrypted].keys.include?(:secret_key) &&
|
14
|
+
data[:encrypted].keys.include?(:contents) &&
|
15
|
+
data[:encrypted][:contents].is_a?(String)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.read(content)
|
19
|
+
file { |f| f.read }
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.write(content, file = nil)
|
23
|
+
if file
|
24
|
+
File.open(file, "w") do |f|
|
25
|
+
f << content
|
26
|
+
end
|
27
|
+
else
|
28
|
+
file("w") { |f| f << content }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.file(mode = "r", &block)
|
33
|
+
File.open(DefaultStorageFile, mode, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/syde/vault.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
module Syde
|
2
|
+
class Vault
|
3
|
+
include Errors
|
4
|
+
|
5
|
+
attr_accessor :plaintext_secret_key
|
6
|
+
attr_reader :file
|
7
|
+
|
8
|
+
def self.open(file = Storage::DefaultStorageFile)
|
9
|
+
file = File.expand_path(file)
|
10
|
+
FileUtils.touch(file) unless File.exist?(file)
|
11
|
+
|
12
|
+
Vault.new(YAML.load_file(file) || "", file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.create(password, file = Storage::DefaultStorageFile)
|
16
|
+
file = File.expand_path(file)
|
17
|
+
|
18
|
+
raise "#{file} contains content -- refusing to override." if File.exist?(file) && File.size(file) > 0
|
19
|
+
|
20
|
+
FileUtils.touch(file) unless File.exist?(file)
|
21
|
+
|
22
|
+
h = {}
|
23
|
+
[:plaintext, :encrypted].each { |e| h[e] = {} }
|
24
|
+
|
25
|
+
h[:plaintext][:iv] = Crypto.new_iv
|
26
|
+
new_secret_key = Vault.new_secret_key(password, h[:plaintext][:iv])
|
27
|
+
encrypted_key = new_secret_key[:encrypted_key]
|
28
|
+
hash = new_secret_key[:plaintext_key_hash]
|
29
|
+
plaintext_key = new_secret_key[:plaintext_key]
|
30
|
+
|
31
|
+
h[:encrypted][:secret_key] = encrypted_key
|
32
|
+
h[:plaintext][:secret_key_hash] = hash
|
33
|
+
|
34
|
+
h[:encrypted][:contents] = Crypto.encrypt(plaintext_key, h[:plaintext][:iv], YAML.dump([]))
|
35
|
+
h[:plaintext][:contents] = []
|
36
|
+
|
37
|
+
File.open(file, "w") do |f|
|
38
|
+
f << YAML.dump(h)
|
39
|
+
end
|
40
|
+
|
41
|
+
Vault.new(h, file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(data, file)
|
45
|
+
@data = data
|
46
|
+
@file = file.freeze
|
47
|
+
|
48
|
+
raise ArgumentError, "unable to find any stored data." if @data.empty?
|
49
|
+
raise ArgumentError, "data is not valid." unless Storage.valid_format?(@data)
|
50
|
+
end
|
51
|
+
|
52
|
+
def data
|
53
|
+
if locked?
|
54
|
+
public_data
|
55
|
+
else
|
56
|
+
internal_data
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def public_data
|
61
|
+
public_data = YAML.load(YAML.dump(internal_data))
|
62
|
+
public_data[:plaintext].delete(:contents)
|
63
|
+
public_data
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def internal_data
|
69
|
+
@data
|
70
|
+
end
|
71
|
+
|
72
|
+
public
|
73
|
+
|
74
|
+
def iv
|
75
|
+
internal_data[:plaintext][:iv]
|
76
|
+
end
|
77
|
+
|
78
|
+
def secret_key_hash
|
79
|
+
internal_data[:plaintext][:secret_key_hash]
|
80
|
+
end
|
81
|
+
|
82
|
+
def decrypt_secret_key(password)
|
83
|
+
Crypto.aes(:decrypt, password, iv, internal_data[:encrypted][:secret_key])
|
84
|
+
end
|
85
|
+
|
86
|
+
def lock
|
87
|
+
internal_data[:encrypted][:contents] = Crypto.encrypt(@plaintext_secret_key, iv, YAML.dump(internal_data[:plaintext][:contents]))
|
88
|
+
internal_data[:plaintext][:contents] = nil
|
89
|
+
@plaintext_secret_key = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def unlock!(password = nil)
|
93
|
+
raise MissingPasswordError, "no password given." unless password
|
94
|
+
|
95
|
+
plaintext_secret_key = decrypt_secret_key(password)
|
96
|
+
if Crypto.digest(plaintext_secret_key) != secret_key_hash
|
97
|
+
raise PasswordIncorrectError
|
98
|
+
else
|
99
|
+
@plaintext_secret_key = plaintext_secret_key
|
100
|
+
internal_data[:encrypted][:contents] ||= Crypto.encrypt(@plaintext_secret_key, iv, YAML.dump([]))
|
101
|
+
internal_data[:plaintext][:contents] = YAML.load(Crypto.decrypt(@plaintext_secret_key, iv, internal_data[:encrypted][:contents]))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def unlock(password = nil, timeout = 5 * 60)
|
106
|
+
return false unless timeout > 0
|
107
|
+
unlock!(password)
|
108
|
+
start_locking_timer(timeout)
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
def start_locking_timer(seconds)
|
113
|
+
Thread.new do
|
114
|
+
sleep seconds
|
115
|
+
self.lock
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def locked?
|
120
|
+
if plaintext_secret_key
|
121
|
+
false
|
122
|
+
else
|
123
|
+
true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def plaintext_contents
|
128
|
+
raise AccessError, "vault is locked; unable to access vault contents." if locked?
|
129
|
+
|
130
|
+
YAML.load(YAML.dump(internal_contents))
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def internal_contents
|
136
|
+
internal_data[:plaintext][:contents]
|
137
|
+
end
|
138
|
+
|
139
|
+
public
|
140
|
+
|
141
|
+
def contents
|
142
|
+
if locked?
|
143
|
+
public_contents
|
144
|
+
else
|
145
|
+
plaintext_contents
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def public_contents
|
150
|
+
raise AccessError, "vault is locked; unable to access vault contents." if locked?
|
151
|
+
|
152
|
+
public_data[:plaintext][:contents]
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def update_contents(new_content)
|
158
|
+
internal_data[:encrypted][:contents] = Crypto.encrypt(@plaintext_secret_key, iv, YAML.dump(internal_contents))
|
159
|
+
|
160
|
+
Storage.write(YAML.dump(public_data), file)
|
161
|
+
|
162
|
+
plaintext_contents
|
163
|
+
end
|
164
|
+
|
165
|
+
public
|
166
|
+
|
167
|
+
def contents=(new_content)
|
168
|
+
raise AccessError, "vault is locked; unable to modify vault contents." if locked?
|
169
|
+
|
170
|
+
internal_contents.replace(new_content)
|
171
|
+
|
172
|
+
update_contents(new_content)
|
173
|
+
end
|
174
|
+
|
175
|
+
def add(*contents)
|
176
|
+
raise AccessError, "vault is locked; unable to add content to vault." if locked?
|
177
|
+
|
178
|
+
contents.each do |content|
|
179
|
+
internal_contents << content
|
180
|
+
end
|
181
|
+
|
182
|
+
update_contents(internal_contents)
|
183
|
+
end
|
184
|
+
alias_method :<<, :add
|
185
|
+
|
186
|
+
def remove(*contents)
|
187
|
+
raise AccessError, "vault is locked; unable to remove content from vault." if locked?
|
188
|
+
|
189
|
+
contents.each do |content|
|
190
|
+
internal_contents.delete(content)
|
191
|
+
end
|
192
|
+
|
193
|
+
update_contents(internal_contents)
|
194
|
+
end
|
195
|
+
|
196
|
+
def status
|
197
|
+
locked? ? "locked" : "unlocked"
|
198
|
+
end
|
199
|
+
|
200
|
+
def inspect
|
201
|
+
%Q{#<Vault (#{status})>}
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.new_secret_key(password, iv)
|
205
|
+
plaintext = Crypto.digest(Crypto.random_bytes(4096))
|
206
|
+
new_key = Crypto.aes(:encrypt, password, iv, plaintext)
|
207
|
+
#? plaintext = nil
|
208
|
+
#? GC.start
|
209
|
+
{ :encrypted_key => new_key,
|
210
|
+
:plaintext_key_hash => Crypto.digest(plaintext),
|
211
|
+
:plaintext_key => plaintext }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/lib/syde/vault.rb~
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
module Syde
|
2
|
+
class Vault
|
3
|
+
include Errors
|
4
|
+
|
5
|
+
attr_accessor :plaintext_secret_key
|
6
|
+
attr_reader :file
|
7
|
+
|
8
|
+
def self.open(file = Storage::DefaultStorageFile)
|
9
|
+
file = File.expand_path(file)
|
10
|
+
FileUtils.touch(file) unless File.exist?(file)
|
11
|
+
|
12
|
+
Vault.new(YAML.load_file(file) || "", file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.create(password, file = Storage::DefaultStorageFile)
|
16
|
+
file = File.expand_path(file)
|
17
|
+
|
18
|
+
raise "#{file} contains content -- refusing to override." if File.exist?(file) && File.size(file) > 0
|
19
|
+
|
20
|
+
FileUtils.touch(file) unless File.exist?(file)
|
21
|
+
|
22
|
+
h = {}
|
23
|
+
[:plaintext, :encrypted].each { |e| h[e] = {} }
|
24
|
+
|
25
|
+
h[:plaintext][:iv] = Crypto.new_iv
|
26
|
+
new_secret_key = Vault.new_secret_key(password, h[:plaintext][:iv])
|
27
|
+
encrypted_key = new_secret_key[:encrypted_key]
|
28
|
+
hash = new_secret_key[:plaintext_key_hash]
|
29
|
+
plaintext_key = new_secret_key[:plaintext_key]
|
30
|
+
|
31
|
+
h[:encrypted][:secret_key] = encrypted_key
|
32
|
+
h[:plaintext][:secret_key_hash] = hash
|
33
|
+
|
34
|
+
h[:encrypted][:contents] = Crypto.encrypt(plaintext_key, h[:plaintext][:iv], YAML.dump([]))
|
35
|
+
h[:plaintext][:contents] = []
|
36
|
+
|
37
|
+
File.open(file, "w") do |f|
|
38
|
+
f << YAML.dump(h)
|
39
|
+
end
|
40
|
+
|
41
|
+
Vault.new(h, file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(data, file)
|
45
|
+
@data = data
|
46
|
+
@file = file.freeze
|
47
|
+
|
48
|
+
raise ArgumentError, "unable to find any stored data." if @data.empty?
|
49
|
+
raise ArgumentError, "data is not valid." unless Storage.valid_format?(@data)
|
50
|
+
end
|
51
|
+
|
52
|
+
def data
|
53
|
+
if locked?
|
54
|
+
public_data
|
55
|
+
else
|
56
|
+
internal_data
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def public_data
|
61
|
+
public_data = YAML.load(YAML.dump(internal_data))
|
62
|
+
public_data[:plaintext].delete(:contents)
|
63
|
+
public_data
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def internal_data
|
69
|
+
@data
|
70
|
+
end
|
71
|
+
|
72
|
+
public
|
73
|
+
|
74
|
+
def iv
|
75
|
+
internal_data[:plaintext][:iv]
|
76
|
+
end
|
77
|
+
|
78
|
+
def secret_key_hash
|
79
|
+
internal_data[:plaintext][:secret_key_hash]
|
80
|
+
end
|
81
|
+
|
82
|
+
def decrypt_secret_key(password)
|
83
|
+
Crypto.aes(:decrypt, password, iv, internal_data[:encrypted][:secret_key])
|
84
|
+
end
|
85
|
+
|
86
|
+
def lock
|
87
|
+
internal_data[:encrypted][:contents] = Crypto.encrypt(@plaintext_secret_key, iv, YAML.dump(internal_data[:plaintext][:contents]))
|
88
|
+
internal_data[:plaintext][:contents] = nil
|
89
|
+
@plaintext_secret_key = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def unlock!(password = nil)
|
93
|
+
raise MissingPasswordError, "no password given." unless password
|
94
|
+
|
95
|
+
plaintext_secret_key = decrypt_secret_key(password)
|
96
|
+
if Crypto.digest(plaintext_secret_key) != secret_key_hash
|
97
|
+
raise PasswordIncorrectError
|
98
|
+
else
|
99
|
+
@plaintext_secret_key = plaintext_secret_key
|
100
|
+
internal_data[:encrypted][:contents] ||= Crypto.encrypt(@plaintext_secret_key, iv, YAML.dump([]))
|
101
|
+
internal_data[:plaintext][:contents] = YAML.load(Crypto.decrypt(@plaintext_secret_key, iv, internal_data[:encrypted][:contents]))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def unlock(password = nil, timeout = 5 * 60)
|
106
|
+
return false unless timeout > 0
|
107
|
+
unlock!(password)
|
108
|
+
start_locking_timer(timeout)
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
def start_locking_timer(seconds)
|
113
|
+
Thread.new do
|
114
|
+
sleep seconds
|
115
|
+
self.lock
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def locked?
|
120
|
+
if plaintext_secret_key
|
121
|
+
false
|
122
|
+
else
|
123
|
+
true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def plaintext_contents
|
128
|
+
raise AccessError, "vault is locked; unable to access vault contents." if locked?
|
129
|
+
|
130
|
+
YAML.load(YAML.dump(internal_contents))
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def internal_contents
|
136
|
+
internal_data[:plaintext][:contents]
|
137
|
+
end
|
138
|
+
|
139
|
+
public
|
140
|
+
|
141
|
+
def contents
|
142
|
+
if locked?
|
143
|
+
public_contents
|
144
|
+
else
|
145
|
+
plaintext_contents
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def public_contents
|
150
|
+
raise AccessError, "vault is locked; unable to access vault contents." if locked?
|
151
|
+
|
152
|
+
public_data[:plaintext][:contents]
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def update_contents(new_content)
|
158
|
+
internal_data[:encrypted][:contents] = Crypto.encrypt(@plaintext_secret_key, iv, YAML.dump(internal_contents))
|
159
|
+
|
160
|
+
Storage.write(YAML.dump(public_data), file)
|
161
|
+
|
162
|
+
plaintext_contents
|
163
|
+
end
|
164
|
+
|
165
|
+
public
|
166
|
+
|
167
|
+
def contents=(new_content)
|
168
|
+
raise AccessError, "vault is locked; unable to modify vault contents." if locked?
|
169
|
+
|
170
|
+
internal_contents.replace(new_content)
|
171
|
+
|
172
|
+
update_contents(new_content)
|
173
|
+
end
|
174
|
+
|
175
|
+
def add(*contents)
|
176
|
+
raise AccessError, "vault is locked; unable to add content to vault." if locked?
|
177
|
+
|
178
|
+
contents.each do |content|
|
179
|
+
internal_contents << content
|
180
|
+
end
|
181
|
+
|
182
|
+
update_contents(internal_contents)
|
183
|
+
end
|
184
|
+
alias_method :<<, :add
|
185
|
+
|
186
|
+
def remove(*contents)
|
187
|
+
raise AccessError, "vault is locked; unable to remove content from vault." if locked?
|
188
|
+
|
189
|
+
contents.each do |content|
|
190
|
+
internal_contents.delete(content)
|
191
|
+
end
|
192
|
+
|
193
|
+
update_contents(internal_contents)
|
194
|
+
end
|
195
|
+
|
196
|
+
def status
|
197
|
+
locked? ? "locked" : "unlocked"
|
198
|
+
end
|
199
|
+
|
200
|
+
def inspect
|
201
|
+
%Q{#<Vault (#{status})>}
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.new_secret_key(password, iv)
|
205
|
+
plaintext = Crypto.digest(Crypto.random_bytes(4096))
|
206
|
+
new_key = Crypto.aes(:encrypt, password, iv, plaintext)
|
207
|
+
#? plaintext = nil
|
208
|
+
#? GC.start
|
209
|
+
{ :encrypted_key => new_key,
|
210
|
+
:plaintext_key_hash => Crypto.digest(plaintext),
|
211
|
+
:plaintext_key => plaintext }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/lib/syde.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "yaml"
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
# http://snippets.dzone.com/posts/show/576
|
6
|
+
# http://snippets.dzone.com/posts/show/4975
|
7
|
+
|
8
|
+
FileUtils.mkdir(File.expand_path("~/.syde")) unless File.exist?(File.expand_path("~/.syde"))
|
9
|
+
|
10
|
+
module Syde
|
11
|
+
SYDE_VERSION_MAJOR = "0"
|
12
|
+
SYDE_VERSION_MINOR = "0"
|
13
|
+
SYDE_VERSION_TINY = "1"
|
14
|
+
|
15
|
+
SYDE_VERSION = [SYDE_VERSION_MAJOR, SYDE_VERSION_MINOR, SYDE_VERSION_TINY].join(".")
|
16
|
+
|
17
|
+
SYDE_VERSION_DATE = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
require "syde/crypto"
|
21
|
+
require "syde/errors"
|
22
|
+
require "syde/storage"
|
23
|
+
require "syde/vault"
|
data/lib/syde.rb~
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "yaml"
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
# http://snippets.dzone.com/posts/show/576
|
6
|
+
# http://snippets.dzone.com/posts/show/4975
|
7
|
+
|
8
|
+
FileUtils.mkdir(File.expand_path("~/.syde")) unless File.exist?(File.expand_path("~/.syde"))
|
9
|
+
|
10
|
+
module Syde
|
11
|
+
SYDE_VERSION_MAJOR = "0"
|
12
|
+
SYDE_VERSION_MINOR = "0"
|
13
|
+
SYDE_VERSION_TINY = "1"
|
14
|
+
|
15
|
+
SYDE_VERSION = [SYDE_VERSION_MAJOR, SYDE_VERSION_MINOR, SYDE_VERSION_TINY].join(".")
|
16
|
+
|
17
|
+
SYDE_VERSION_DATE = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
require "syde/crypto"
|
21
|
+
require "syde/errors"
|
22
|
+
require "syde/storage"
|
23
|
+
require "syde/vault"
|
data/rakefile
ADDED
data/syde.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "syde"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.authors = ["Adam Prescott"]
|
5
|
+
s.email = ["adam@aprescott.com"]
|
6
|
+
s.homepage = "https://github.com/aprescott/syde"
|
7
|
+
s.summary = "Symmetric data encryption library."
|
8
|
+
s.description = "Syde is a symmetric data encryption library written in Ruby, licensed under the MIT license. It provides a saved encrypted data storage under a single password."
|
9
|
+
s.files = Dir["{lib/**/*,test/**/*}"] + %w[LICENSE.mit Gemfile rakefile README.md syde.gemspec .gemtest]
|
10
|
+
s.require_path = "lib"
|
11
|
+
s.test_files = Dir["test/*"]
|
12
|
+
s.has_rdoc = false
|
13
|
+
s.add_development_dependency "rake"
|
14
|
+
s.required_ruby_version = "~> 1.8.7"
|
15
|
+
s.requirements << "Ruby 1.8.7, does not work with 1.9 (yet) due to encodings"
|
16
|
+
end
|
data/test/_test.rb
ADDED
data/test/test_crypto.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
class TC_Syde_Crypto < Test::Unit::TestCase
|
2
|
+
def setup
|
3
|
+
FileUtils.rm(TEST_STORAGE_FILE)
|
4
|
+
@vault = Syde::Vault.create("test_password", TEST_STORAGE_FILE)
|
5
|
+
end
|
6
|
+
|
7
|
+
def tear_down
|
8
|
+
FileUtils.rm(TEST_STORAGE_FILE)
|
9
|
+
end
|
10
|
+
|
11
|
+
def iv
|
12
|
+
@vault.iv
|
13
|
+
end
|
14
|
+
|
15
|
+
def encrypt(key, plaintext)
|
16
|
+
Syde::Vault::Crypto.encrypt(key, iv, plaintext)
|
17
|
+
end
|
18
|
+
|
19
|
+
def decrypt(key, ciphertext)
|
20
|
+
Syde::Vault::Crypto.decrypt(key, iv, ciphertext)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_invertible
|
24
|
+
assert_equal "test_plaintext", decrypt("test_key", encrypt("test_key", "test_plaintext"))
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_secret_key_length
|
28
|
+
Syde::Vault::Crypto.random_bytes(1024).size == 1024
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class TC_Syde_Storage < Test::Unit::TestCase
|
2
|
+
TEST_DATA = { :plaintext => { :iv => "test iv", :secret_key_hash => "test secret key hash" },
|
3
|
+
:encrypted => { :secret_key => "test secret key",
|
4
|
+
:contents => "" } }
|
5
|
+
|
6
|
+
def setup
|
7
|
+
FileUtils.rm(TEST_STORAGE_FILE) if File.exist?(TEST_STORAGE_FILE)
|
8
|
+
@vault = Syde::Vault.create("test_password", TEST_STORAGE_FILE)
|
9
|
+
end
|
10
|
+
|
11
|
+
def valid_format?(d)
|
12
|
+
Syde::Vault::Storage.valid_format?(d)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_valid_format
|
16
|
+
assert valid_format?(TEST_DATA), "data intended to be in correct format was found invalid"
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_internal_valid_format
|
20
|
+
@vault.unlock!("test_password")
|
21
|
+
assert valid_format?(@vault.data), "internal inconsistency"
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_data_copy
|
25
|
+
Marshal.load(Marshal.dump(TEST_DATA))
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_invalid_format
|
29
|
+
invalid_formats = {
|
30
|
+
[] => "annot be an array",
|
31
|
+
nil => "cannot be nil",
|
32
|
+
{} => "contains nothing",
|
33
|
+
}
|
34
|
+
|
35
|
+
invalid_formats.each do |d, result|
|
36
|
+
assert !valid_format?(d), result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_missing_plaintext
|
41
|
+
d = test_data_copy
|
42
|
+
d.delete(:plaintext)
|
43
|
+
assert !valid_format?(d), "data is incorrect format: :plaintext key is missing"
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_missing_encrypted
|
47
|
+
d = test_data_copy
|
48
|
+
d.delete(:encrypted)
|
49
|
+
assert !valid_format?(d), "data is incorrect format: :encrypted key is missing"
|
50
|
+
end
|
51
|
+
def test_missing_encrypted_contents
|
52
|
+
d = test_data_copy
|
53
|
+
d[:encrypted].delete(:contents)
|
54
|
+
assert !valid_format?(d), "data is incorrect format: missing encrypted :contents"
|
55
|
+
end
|
56
|
+
def test_encrypted_contents_is_string
|
57
|
+
d = test_data_copy
|
58
|
+
d[:encrypted][:contents] = {}
|
59
|
+
assert !valid_format?(d), "data is incorrect format: encrypted contents must be a string"
|
60
|
+
|
61
|
+
d = test_data_copy
|
62
|
+
d[:encrypted][:contents] = []
|
63
|
+
assert !valid_format?(d) => "data is incorrect format: encrypted contents must be a string"
|
64
|
+
end
|
65
|
+
def test_missing_encrypted_secret_key
|
66
|
+
d = test_data_copy
|
67
|
+
d[:encrypted].delete(:secret_key)
|
68
|
+
assert !valid_format?(d), "data is incorrect format: missing encrypted :secret_key"
|
69
|
+
end
|
70
|
+
def test_missing_plaintext_iv
|
71
|
+
d = test_data_copy
|
72
|
+
d[:plaintext].delete(:iv)
|
73
|
+
assert !valid_format?(d), "data is incorrect format: missing plaintext :iv"
|
74
|
+
end
|
75
|
+
def test_missing_secret_key_hash
|
76
|
+
d = test_data_copy
|
77
|
+
d[:plaintext].delete(:secret_key_hash)
|
78
|
+
assert !valid_format?(d), "data is incorrect format: missing secret key hash"
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_open_storage_file
|
82
|
+
assert Syde::Vault.open(TEST_STORAGE_FILE)
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_empty_data_failure
|
86
|
+
assert_raise ArgumentError do
|
87
|
+
Syde::Vault.new("")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/test/test_vault.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
class TC_Syde_Vault < Test::Unit::TestCase
|
2
|
+
def setup
|
3
|
+
FileUtils.rm(TEST_STORAGE_FILE) if File.exist?(TEST_STORAGE_FILE)
|
4
|
+
@vault = Syde::Vault.create("test_password", TEST_STORAGE_FILE)
|
5
|
+
end
|
6
|
+
|
7
|
+
def tear_down
|
8
|
+
FileUtils.rm(TEST_STORAGE_FILE) if File.exist?(TEST_STORAGE_FILE)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_initial_contents_empty
|
12
|
+
@vault.unlock!("test_password")
|
13
|
+
assert @vault.contents.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_vault_add
|
17
|
+
@vault.unlock!("test_password")
|
18
|
+
assert @vault.add("1")
|
19
|
+
assert @vault.contents == ["1"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_vault_multiple_add
|
23
|
+
@vault.unlock!("test_password")
|
24
|
+
assert @vault.add("1", 3, /regex/)
|
25
|
+
@vault.contents.each do |e|
|
26
|
+
assert [/regex/, 3, "1"].include?(e)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_begins_locked
|
31
|
+
assert @vault.locked?
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_non_override_of_storage_file
|
35
|
+
File.open(TEST_STORAGE_FILE, "w") do |f|
|
36
|
+
f.write "test"
|
37
|
+
end
|
38
|
+
assert_raise RuntimeError do
|
39
|
+
Syde::Vault.create("test_password", TEST_STORAGE_FILE)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_unlock_incorrect_password
|
44
|
+
assert_raise OpenSSL::Cipher::CipherError, Syde::Errors::PasswordIncorrectError do
|
45
|
+
@vault.unlock!("incorrect_password")
|
46
|
+
end
|
47
|
+
assert @vault.locked?
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_unlock_correct_password
|
51
|
+
@vault.unlock!("test_password")
|
52
|
+
assert !@vault.locked?
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_auto_locks
|
56
|
+
assert @vault.locked?
|
57
|
+
@vault.unlock("test_password", 1)
|
58
|
+
assert !@vault.locked?
|
59
|
+
sleep 2
|
60
|
+
assert @vault.locked?
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_instant_lock
|
64
|
+
assert @vault.locked?
|
65
|
+
@vault.unlock("test_password", 0)
|
66
|
+
assert @vault.locked?
|
67
|
+
end
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: syde
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Adam Prescott
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-17 00:00:00 +01:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rake
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :development
|
26
|
+
version_requirements: *id001
|
27
|
+
description: Syde is a symmetric data encryption library written in Ruby, licensed under the MIT license. It provides a saved encrypted data storage under a single password.
|
28
|
+
email:
|
29
|
+
- adam@aprescott.com
|
30
|
+
executables: []
|
31
|
+
|
32
|
+
extensions: []
|
33
|
+
|
34
|
+
extra_rdoc_files: []
|
35
|
+
|
36
|
+
files:
|
37
|
+
- lib/syde.rb
|
38
|
+
- lib/syde.rb~
|
39
|
+
- lib/syde/vault.rb
|
40
|
+
- lib/syde/vault.rb~
|
41
|
+
- lib/syde/errors.rb
|
42
|
+
- lib/syde/storage.rb
|
43
|
+
- lib/syde/crypto.rb
|
44
|
+
- test/test_storage.rb
|
45
|
+
- test/test_vault.rb
|
46
|
+
- test/_test.rb
|
47
|
+
- test/test_crypto.rb
|
48
|
+
- LICENSE.mit
|
49
|
+
- Gemfile
|
50
|
+
- rakefile
|
51
|
+
- README.md
|
52
|
+
- syde.gemspec
|
53
|
+
- .gemtest
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: https://github.com/aprescott/syde
|
56
|
+
licenses: []
|
57
|
+
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.8.7
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
requirements:
|
76
|
+
- Ruby 1.8.7, does not work with 1.9 (yet) due to encodings
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.6.2
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: Symmetric data encryption library.
|
82
|
+
test_files:
|
83
|
+
- test/test_storage.rb
|
84
|
+
- test/test_vault.rb
|
85
|
+
- test/_test.rb
|
86
|
+
- test/test_crypto.rb
|