clandestine 0.0.1 → 0.1.0
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 +4 -4
- data/.gitignore +2 -9
- data/.rspec +2 -0
- data/README.md +30 -18
- data/Rakefile +0 -24
- data/bin/clandestine +1 -1
- data/clandestine.gemspec +3 -1
- data/lib/clandestine.rb +1 -9
- data/lib/clandestine/clandestine_error.rb +4 -0
- data/lib/clandestine/command_line_options.rb +52 -0
- data/lib/clandestine/command_line_runner.rb +86 -0
- data/lib/clandestine/commands.rb +52 -0
- data/lib/clandestine/commands/add.rb +29 -0
- data/lib/clandestine/commands/delete.rb +25 -0
- data/lib/clandestine/commands/get.rb +37 -0
- data/lib/clandestine/commands/remove_safe.rb +18 -0
- data/lib/clandestine/commands/update.rb +32 -0
- data/lib/clandestine/crypt.rb +44 -21
- data/lib/clandestine/io.rb +32 -0
- data/lib/clandestine/password_generator.rb +18 -0
- data/lib/clandestine/safe.rb +71 -44
- data/lib/clandestine/safe_authentication.rb +18 -0
- data/lib/clandestine/safe_location.rb +3 -0
- data/lib/clandestine/version.rb +1 -1
- data/spec/commands_spec.rb +98 -0
- data/spec/crypt_spec.rb +20 -5
- data/spec/safe_authentication_spec.rb +30 -0
- data/spec/spec_helper.rb +0 -8
- metadata +50 -12
- data/lib/clandestine/config.rb +0 -25
- data/lib/clandestine/guard.rb +0 -217
- data/spec/config_spec.rb +0 -47
- data/spec/guard_spec.rb +0 -127
- data/spec/safe_spec.rb +0 -40
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative '../safe'
|
2
|
+
|
3
|
+
module Clandestine
|
4
|
+
class RemoveSafe
|
5
|
+
attr_reader :safe_password
|
6
|
+
private :safe_password
|
7
|
+
|
8
|
+
def initialize(safe_password)
|
9
|
+
@safe_password = safe_password
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove
|
13
|
+
Safe.new(safe_password).open do |safe|
|
14
|
+
safe.remove
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative '../safe'
|
2
|
+
require_relative '../io'
|
3
|
+
require_relative '../password_generator'
|
4
|
+
|
5
|
+
module Clandestine
|
6
|
+
module Commands
|
7
|
+
class Update
|
8
|
+
attr_reader :safe_password, :key
|
9
|
+
private :safe_password, :key
|
10
|
+
|
11
|
+
def initialize(safe_password, key = nil)
|
12
|
+
@safe_password = safe_password
|
13
|
+
@key = key.to_sym if key
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
Safe.new(safe_password).open do |safe|
|
18
|
+
if key
|
19
|
+
if safe[key]
|
20
|
+
safe.add(key, PasswordGenerator.random_password)
|
21
|
+
else
|
22
|
+
false
|
23
|
+
end
|
24
|
+
else
|
25
|
+
new_password = IO.get_password(true).chomp
|
26
|
+
safe.update_safe_password(new_password)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/clandestine/crypt.rb
CHANGED
@@ -1,29 +1,52 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'bcrypt'
|
3
|
+
require_relative 'clandestine_error'
|
4
|
+
|
1
5
|
module Clandestine
|
2
|
-
# This module provides encrypt
|
3
|
-
# and decrypt methods using aes-256-cbc
|
4
6
|
module Crypt
|
5
|
-
|
6
|
-
|
7
|
-
Base64.encode64 aes(:encrypt, password, data)
|
7
|
+
def self.hash_password(password)
|
8
|
+
BCrypt::Password.create(password).b
|
8
9
|
end
|
9
|
-
|
10
|
-
def
|
11
|
-
|
10
|
+
|
11
|
+
def self.matches(hash, password)
|
12
|
+
BCrypt::Password.new(hash) == password
|
12
13
|
end
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
|
15
|
+
def self.encrypt(data, password)
|
16
|
+
cipher = aes(:encrypt)
|
17
|
+
cipher.iv = iv = cipher.random_iv
|
18
|
+
key_len = cipher.key_len
|
19
|
+
salt = OpenSSL::Random.random_bytes 16
|
20
|
+
cipher.key = key = get_key(password, key_len, salt)
|
21
|
+
encrypted = cipher.update(data) << cipher.final
|
22
|
+
Base64.encode64 encrypted << salt << iv
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.decrypt(data, password)
|
26
|
+
data = Base64.decode64 data
|
27
|
+
cipher = aes(:decrypt)
|
28
|
+
cipher.iv = data.slice! -16..-1
|
29
|
+
key_len = cipher.key_len
|
30
|
+
salt = data.slice! -16..-1
|
31
|
+
cipher.key = get_key(password,key_len, salt)
|
32
|
+
cipher.update(data) << cipher.final
|
33
|
+
rescue OpenSSL::Cipher::CipherError
|
34
|
+
raise ClandestineError, 'Invalid password!'
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.aes(type)
|
22
40
|
aes = OpenSSL::Cipher.new("AES-256-CBC")
|
23
|
-
aes.send
|
24
|
-
aes
|
25
|
-
|
26
|
-
|
41
|
+
aes.send type
|
42
|
+
aes
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.get_key(password, length, salt)
|
46
|
+
iter = 20000
|
47
|
+
digest = OpenSSL::Digest::SHA256.new
|
48
|
+
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iter, length, digest)
|
27
49
|
end
|
50
|
+
|
28
51
|
end
|
29
52
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
|
3
|
+
module Clandestine
|
4
|
+
class IO
|
5
|
+
def self.copy_to_clipboard(value)
|
6
|
+
::IO.popen('pbcopy', 'w') { |b| b.print "#{value}" }
|
7
|
+
say("Password on clipboard countdown: ")
|
8
|
+
10.downto(1) do |n|
|
9
|
+
sleep(1);
|
10
|
+
n == 1 ? say("1") : say("#{n} ")
|
11
|
+
end
|
12
|
+
::IO.popen('pbcopy', 'w') { |b| b.print "" }
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.print_keys(contents)
|
16
|
+
puts "-------------"
|
17
|
+
puts "Safe Contents"
|
18
|
+
puts "-------------"
|
19
|
+
contents.sort.each { |key| puts key.to_s }
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.get_password(safe_password = false)
|
23
|
+
message = if safe_password
|
24
|
+
"Enter the new safe password"
|
25
|
+
else
|
26
|
+
"Enter the safe password"
|
27
|
+
end
|
28
|
+
ask(message) { |q| q.echo = "*" }
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Clandestine
|
2
|
+
class PasswordGenerator
|
3
|
+
LETTERS = ('a'..'z').to_a + ('A'..'Z').to_a
|
4
|
+
NUMS = ('0'..'9').to_a * 5
|
5
|
+
CHARS = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'] * 5
|
6
|
+
|
7
|
+
def self.random_password
|
8
|
+
password = ''
|
9
|
+
generator = Random.new(Random.srand)
|
10
|
+
chars = LETTERS + NUMS + CHARS
|
11
|
+
chars.shuffle!
|
12
|
+
12.times do |n|
|
13
|
+
password << chars[generator.rand(0..chars.size - 1)]
|
14
|
+
end
|
15
|
+
password
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/clandestine/safe.rb
CHANGED
@@ -1,54 +1,81 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
require_relative 'crypt'
|
3
|
+
require_relative 'safe_location'
|
4
|
+
require_relative 'safe_authentication'
|
5
|
+
|
1
6
|
module Clandestine
|
2
|
-
# The Safe class interacts with a file (safe),
|
3
|
-
# storing, retrieving, or removing data from it.
|
4
7
|
class Safe
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@
|
12
|
-
|
13
|
-
if
|
14
|
-
|
8
|
+
|
9
|
+
attr_reader :safe, :password, :location
|
10
|
+
private :safe, :password, :location
|
11
|
+
|
12
|
+
def initialize(password)
|
13
|
+
@password = password
|
14
|
+
@location = ENV['CLANDESTINE_SAFE'] || SAFE_LOCATION
|
15
|
+
@safe = PStore.new(location)
|
16
|
+
if first_access(location)
|
17
|
+
open do |s|
|
18
|
+
safe[:safe_password] = Crypt.hash_password(IO.get_password(true))
|
19
|
+
end
|
15
20
|
end
|
16
21
|
end
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
File.open(@safe_location, 'w') { |f| f << nil }
|
22
|
+
|
23
|
+
def open
|
24
|
+
safe.transaction do
|
25
|
+
yield self
|
22
26
|
end
|
23
27
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
|
29
|
+
def contents
|
30
|
+
contents = safe.roots
|
31
|
+
contents.delete(:safe_password)
|
32
|
+
contents
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](key)
|
36
|
+
return if !exists?(key)
|
37
|
+
SafeAuthentication.authenticate(safe, password)
|
38
|
+
value = safe[key]
|
39
|
+
Crypt.decrypt(value, password)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias :get :[]
|
43
|
+
|
44
|
+
def []=(key, value)
|
45
|
+
SafeAuthentication.authenticate(safe, password)
|
46
|
+
safe[key] = Crypt.encrypt value, password
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
alias :add :[]=
|
51
|
+
|
52
|
+
def delete(key)
|
53
|
+
SafeAuthentication.authenticate(safe, password)
|
54
|
+
safe.delete(key)
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_safe_password(new_password)
|
59
|
+
SafeAuthentication.authenticate(safe, password)
|
60
|
+
safe[:safe_password] = Crypt.hash_password(new_password)
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def remove
|
65
|
+
SafeAuthentication.authenticate(safe, password)
|
66
|
+
File.delete location
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def exists?(key)
|
71
|
+
SafeAuthentication.authenticate(safe, password)
|
72
|
+
!safe[key].nil?
|
32
73
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
# Unlocks the safe by decrypting the data
|
41
|
-
# currently in the safe with the specified password
|
42
|
-
# If the password is incorrect an OpenSSL::Cipher::CipherError
|
43
|
-
# exception is thrown
|
44
|
-
def unlock(password)
|
45
|
-
decrypt(IO.readlines(@safe_location).join, password) unless File.zero? @safe_location
|
46
|
-
end
|
47
|
-
|
48
|
-
# Locks the safe by encrypting the data with the
|
49
|
-
# specified password
|
50
|
-
def lock(data, password)
|
51
|
-
File.open(@safe_location, 'w') { |f| f << encrypt(data, password) }
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def first_access(location)
|
78
|
+
!File.exists?(location) || ::IO.readlines(location).empty?
|
52
79
|
end
|
53
80
|
end
|
54
81
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'crypt'
|
2
|
+
require_relative 'clandestine_error'
|
3
|
+
|
4
|
+
module Clandestine
|
5
|
+
class SafeAuthentication
|
6
|
+
|
7
|
+
def self.authenticate(safe, password)
|
8
|
+
abort 'Invalid password!' unless authenticated?(safe, password)
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def self.authenticated?(safe, password)
|
15
|
+
Crypt.matches(safe[:safe_password], password)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/clandestine/version.rb
CHANGED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'clandestine/commands'
|
3
|
+
require 'clandestine/crypt'
|
4
|
+
require 'clandestine/safe'
|
5
|
+
require 'bcrypt'
|
6
|
+
require 'pstore'
|
7
|
+
|
8
|
+
module Clandestine
|
9
|
+
describe Commands do
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
@temp_dir = File.dirname(__FILE__) + "/tmp"
|
13
|
+
Dir.mkdir(@temp_dir) unless File.exists? @temp_dir
|
14
|
+
ENV['CLANDESTINE_SAFE'] = "#{@temp_dir}/pswds"
|
15
|
+
store = PStore.new "#{@temp_dir}/pswds"
|
16
|
+
store.transaction do |s|
|
17
|
+
s[:safe_password] = BCrypt::Password.create 'password'
|
18
|
+
s[:gmail] = Crypt.encrypt 'gmail_password', 'password'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
after :each do
|
23
|
+
passwords = "#{@temp_dir}/pswds"
|
24
|
+
File.delete passwords if File.exists? passwords
|
25
|
+
Dir.rmdir @temp_dir if File.exists? @temp_dir
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '.version' do
|
29
|
+
it 'prints version' do
|
30
|
+
Commands.version.should eq VERSION
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '.add' do
|
35
|
+
it 'adds key and password to safe' do
|
36
|
+
PasswordGenerator.should_receive(:random_password).and_return 'twitter_password'
|
37
|
+
Commands.add 'twitter', 'password'
|
38
|
+
Safe.new('password').open do |s|
|
39
|
+
s[:twitter].should eq 'twitter_password'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.get' do
|
45
|
+
context 'without argument' do
|
46
|
+
it 'prints accounts' do
|
47
|
+
Commands.get(nil, 'password').should eq [:gmail]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with argument' do
|
52
|
+
it 'gets password for key from safe' do
|
53
|
+
IO.should_receive(:get_password).and_return "password"
|
54
|
+
Commands.get('gmail').should eq 'gmail_password'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '.delete' do
|
60
|
+
context 'with argument' do
|
61
|
+
it 'deletes key and password from safe' do
|
62
|
+
Commands.delete 'gmail', 'password'
|
63
|
+
Safe.new('password').open do |s|
|
64
|
+
s.contents.should be_empty
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '.update' do
|
71
|
+
it 'updates password related to key' do
|
72
|
+
PasswordGenerator.should_receive(:random_password).and_return 'updated_password'
|
73
|
+
Commands.update 'gmail', 'password'
|
74
|
+
Safe.new('password').open do |s|
|
75
|
+
s[:gmail].should eq 'updated_password'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '.location' do
|
81
|
+
it 'shows current safe location' do
|
82
|
+
Commands.location.should eq "#{@temp_dir}/pswds"
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'defaults to ENV["HOME"]/.pswds' do
|
86
|
+
ENV.delete('CLANDESTINE_SAFE')
|
87
|
+
Commands.location.should eq "#{ENV['HOME']}/.cls"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '.remove' do
|
92
|
+
it 'removes safe from file system' do
|
93
|
+
Commands.remove 'password'
|
94
|
+
File.exists?(ENV['CLANDESTINE_SAFE'] = "#{@temp_dir}/pswds").should eq false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/spec/crypt_spec.rb
CHANGED
@@ -1,11 +1,26 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'clandestine/crypt'
|
2
3
|
|
3
4
|
describe Clandestine::Crypt do
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
|
6
|
+
describe '#encrypt' do
|
7
|
+
it 'encrypts data with password' do
|
8
|
+
encrypted = Clandestine::Crypt.encrypt("myaccountpassword", "safe_password")
|
9
|
+
encrypted.should_not eql "myaccountpassword"
|
10
|
+
encrypted.should_not eql nil
|
11
|
+
end
|
7
12
|
end
|
8
|
-
|
9
|
-
|
13
|
+
|
14
|
+
describe '#decrypt' do
|
15
|
+
it 'decrypts data with password' do
|
16
|
+
decrypted = Clandestine::Crypt.decrypt("OePyNbyDMUbza5zUVor4wWS8Tb5u26FmxGXpC9XENWeMNJYJj1WKJlDOZYxG\nXpSqHSSNaT4dhUsX+T7abgZ1qg==\n", "safe_password")
|
17
|
+
decrypted.should eql "myaccountpassword"
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not decrypt with wrong password' do
|
21
|
+
expect {
|
22
|
+
Clandestine::Crypt.decrypt("OePyNbyDMUbza5zUVor4wWS8Tb5u26FmxGXpC9XENWeMNJYJj1WKJlDOZYxG\nXpSqHSSNaT4dhUsX+T7abgZ1qg==\n", "wrong_password")
|
23
|
+
}.to raise_error Clandestine::ClandestineError
|
24
|
+
end
|
10
25
|
end
|
11
26
|
end
|