lockr 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/lockr +5 -0
- data/lib/lockr.rb +139 -0
- data/lib/lockr/action/add.rb +34 -0
- data/lib/lockr/action/aes.rb +5 -0
- data/lib/lockr/action/base.rb +71 -0
- data/lib/lockr/action/list.rb +28 -0
- data/lib/lockr/action/remove.rb +40 -0
- data/lib/lockr/action/show.rb +40 -0
- data/lib/lockr/encryption/aes.rb +43 -0
- data/lib/lockr/pwdgen.rb +25 -0
- data/lib/lockr/pwdstore.rb +13 -0
- metadata +79 -0
data/bin/lockr
ADDED
data/lib/lockr.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'highline/import'
|
6
|
+
|
7
|
+
require 'lockr/action/add'
|
8
|
+
require 'lockr/action/list'
|
9
|
+
require 'lockr/action/remove'
|
10
|
+
require 'lockr/action/show'
|
11
|
+
require 'lockr/pwdgen'
|
12
|
+
|
13
|
+
class Lockr
|
14
|
+
def run()
|
15
|
+
options = parse_options()
|
16
|
+
validate_options( options)
|
17
|
+
acquire_additional_input( options)
|
18
|
+
process_actions( options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_options()
|
22
|
+
options = {}
|
23
|
+
|
24
|
+
optparse = OptionParser.new do|opts|
|
25
|
+
# Set a banner, displayed at the top
|
26
|
+
# of the help screen.
|
27
|
+
opts.banner = "Usage: lockr.rb [options]"
|
28
|
+
|
29
|
+
# Define the options, and what they do
|
30
|
+
options[:action] = nil
|
31
|
+
opts.on( '-a', '--action ACTION', 'Execute the requested ACTION (add, remove, list, show)' ) do |id|
|
32
|
+
options[:action] = id
|
33
|
+
end
|
34
|
+
|
35
|
+
options[:id] = nil
|
36
|
+
opts.on( '-i', '--id ID', 'the ID of the password set' ) do |id|
|
37
|
+
options[:id] = id
|
38
|
+
end
|
39
|
+
|
40
|
+
options[:keyfile] = nil
|
41
|
+
opts.on( '-k', '--keyfile FILE', 'the FILE to use as key for the password encryption') do |file|
|
42
|
+
options[:keyfile] = file
|
43
|
+
end
|
44
|
+
|
45
|
+
options[:vault] = 'vault.yaml'
|
46
|
+
opts.on( '-v', '--vault FILE', 'FILE is the name of the vault to store the password sets') do |file|
|
47
|
+
options[:vault] = file
|
48
|
+
end
|
49
|
+
|
50
|
+
options[:generatepwd] = nil
|
51
|
+
opts.on( '-g', '--genpwd PARAMS', 'generate a random password (based on the optional PARAMS)') do |params|
|
52
|
+
options[:generatepwd] = params
|
53
|
+
end
|
54
|
+
|
55
|
+
# This displays the help screen, all programs are
|
56
|
+
# assumed to have this option.
|
57
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
58
|
+
puts opts
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parse the command-line. Remember there are two forms
|
64
|
+
# of the parse method. The 'parse' method simply parses
|
65
|
+
# ARGV, while the 'parse!' method parses ARGV and removes
|
66
|
+
# any options found there, as well as any parameters for
|
67
|
+
# the options. What's left is the list of files to resize.
|
68
|
+
optparse.parse!
|
69
|
+
|
70
|
+
options
|
71
|
+
end
|
72
|
+
|
73
|
+
def validate_options( options)
|
74
|
+
if options[:action].nil?
|
75
|
+
puts 'Please provide an action (--action)'
|
76
|
+
exit 1
|
77
|
+
end
|
78
|
+
|
79
|
+
allowed_actions = %w{ a add r remove s show l list}
|
80
|
+
if allowed_actions.index( options[:action]).nil?
|
81
|
+
puts "Allowed actions are add, remove, list and show"
|
82
|
+
exit 2
|
83
|
+
end
|
84
|
+
|
85
|
+
# keyfile is required for all actions other than list
|
86
|
+
if options[:keyfile].nil? and %w{ l list}.index( options[:action]).nil?
|
87
|
+
puts 'Please provide an encryption key file (--keyfile)'
|
88
|
+
exit 3
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def acquire_additional_input( options)
|
93
|
+
# id is required for all actions except list
|
94
|
+
while options[:id].nil? and %w{ l list}.index( options[:action]).nil?
|
95
|
+
options[:id] = ask("Id? ") { |q| }
|
96
|
+
options[:id] = nil if options[:id].strip() == ''
|
97
|
+
end
|
98
|
+
|
99
|
+
# username is required for actions add, remove
|
100
|
+
actions_requiring_username = %w{ a add r remove}
|
101
|
+
while options[:username].nil? and not actions_requiring_username.index( options[:action]).nil?
|
102
|
+
options[:username] = ask("Username? ") { |q| }
|
103
|
+
options[:username] = nil if options[:username].strip == ''
|
104
|
+
end
|
105
|
+
|
106
|
+
# url is optional for add
|
107
|
+
actions_requiring_url = %w{ a add}
|
108
|
+
if options[:url].nil? and not actions_requiring_url.index( options[:action]).nil?
|
109
|
+
options[:url] = ask("Url? ") { |q| }
|
110
|
+
options[:url] = nil if options[:url].strip() == ''
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def process_actions( options)
|
115
|
+
begin
|
116
|
+
case options[:action]
|
117
|
+
when 'a', 'add'
|
118
|
+
if options[:generatepwd].nil?
|
119
|
+
password = ask("Password? ") { |q| q.echo = "x" }
|
120
|
+
else
|
121
|
+
password = PasswordGenerator.new.generate( options[:generatepwd])
|
122
|
+
end
|
123
|
+
|
124
|
+
action = AddAction.new( options[:id], options[:url], options[:username], password, options[:keyfile], options[:vault])
|
125
|
+
when 'r', 'remove'
|
126
|
+
action = RemoveAction.new( options[:id], options[:username], options[:keyfile], options[:vault])
|
127
|
+
when 's', 'show'
|
128
|
+
action = ShowAction.new( options[:id], options[:username], options[:keyfile], options[:vault])
|
129
|
+
when 'l', 'list'
|
130
|
+
action = ListAction.new( options[:keyfile], options[:vault])
|
131
|
+
else
|
132
|
+
puts "Unknown action #{options[:action]}"
|
133
|
+
end
|
134
|
+
rescue OpenSSL::Cipher::CipherError
|
135
|
+
say( "<%= color('Invalid keyfile', :red) %>")
|
136
|
+
exit 42
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'lockr/action/aes'
|
2
|
+
require 'lockr/pwdstore'
|
3
|
+
|
4
|
+
class AddAction < AesAction
|
5
|
+
|
6
|
+
def initialize(id,url,username,pwd,keyfile,vault)
|
7
|
+
keyfilehash = calculate_hash( keyfile)
|
8
|
+
|
9
|
+
pwd_directory = load_from_vault( vault)
|
10
|
+
|
11
|
+
if pwd_directory.has_key?( id)
|
12
|
+
pwd_directory_id = YAML::load(decrypt( pwd_directory[id][:enc], keyfilehash, pwd_directory[id][:salt]))
|
13
|
+
else
|
14
|
+
pwd_directory_id = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
if ( pwd_directory_id.has_key?( username))
|
18
|
+
overwrite = ask( "Password already exists. Update? (y/n) ") { |q| }
|
19
|
+
unless overwrite.downcase == 'y'
|
20
|
+
exit 14
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
new_store = PasswordStore.new( id, url, username, pwd)
|
25
|
+
pwd_directory_id[username] = new_store
|
26
|
+
|
27
|
+
pwd_directory[id] = {}
|
28
|
+
pwd_directory[id][:enc], pwd_directory[id][:salt] = encrypt( pwd_directory_id.to_yaml, keyfilehash)
|
29
|
+
|
30
|
+
save_to_vault( pwd_directory, vault)
|
31
|
+
say("Password saved for ID '<%= color('#{id}', :blue) %>' and user '<%= color('#{username}', :green) %>'")
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'lockr/encryption/aes'
|
3
|
+
|
4
|
+
class BaseAction
|
5
|
+
def calculate_hash( filename)
|
6
|
+
sha1 = OpenSSL::Digest::SHA512.new
|
7
|
+
|
8
|
+
File.open( filename) do |file|
|
9
|
+
buffer = ''
|
10
|
+
|
11
|
+
# Read the file 512 bytes at a time
|
12
|
+
while not file.eof
|
13
|
+
file.read(512, buffer)
|
14
|
+
sha1.update(buffer)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
sha1.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def save_to_vault( storelist, vault)
|
22
|
+
rotate_vault( vault)
|
23
|
+
|
24
|
+
File.open( vault, 'w') do |f|
|
25
|
+
f.write( storelist.to_yaml)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def rotate_vault( vault)
|
30
|
+
return unless File.exists?(vault)
|
31
|
+
|
32
|
+
# move old files first
|
33
|
+
max_files = 2 # = 3 - 1
|
34
|
+
max_files.downto( 0) { |i|
|
35
|
+
|
36
|
+
if i == 0
|
37
|
+
File.rename( vault, "#{vault}_#{i}")
|
38
|
+
else
|
39
|
+
j = i - 1
|
40
|
+
if File.exists?("#{vault}_#{j}")
|
41
|
+
File.rename( "#{vault}_#{j}", "#{vault}_#{i}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# loads the datastructure for the password sets from the file
|
48
|
+
# it looks like this:
|
49
|
+
#
|
50
|
+
# pwd_directory = {
|
51
|
+
# :id => {
|
52
|
+
# :enc => 'encrypted password store list',
|
53
|
+
# :salt => 'salt for decryption'
|
54
|
+
# }
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
# decrypted_store_list = {
|
58
|
+
# :username => PasswordStore
|
59
|
+
# }
|
60
|
+
def load_from_vault( vault)
|
61
|
+
storelist = {}
|
62
|
+
|
63
|
+
if File.exist?( vault)
|
64
|
+
File.open( vault, 'r') do |f|
|
65
|
+
storelist = YAML::load(f)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
storelist
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'lockr/action/aes'
|
2
|
+
require 'lockr/pwdstore'
|
3
|
+
|
4
|
+
class ListAction < AesAction
|
5
|
+
|
6
|
+
def initialize( keyfile, vault)
|
7
|
+
pwd_directory = load_from_vault( vault)
|
8
|
+
out = []
|
9
|
+
|
10
|
+
if keyfile.nil?
|
11
|
+
pwd_directory.each { |id,value|
|
12
|
+
out << "Id: #{id}"
|
13
|
+
}
|
14
|
+
else
|
15
|
+
keyfilehash = calculate_hash( keyfile)
|
16
|
+
pwd_directory.each { |oid,value|
|
17
|
+
pwd_directory_id = YAML::load(decrypt( value[:enc], keyfilehash, value[:salt]))
|
18
|
+
pwd_directory_id.each { |username, pwdstore|
|
19
|
+
out << "Id: #{pwdstore.id} / Username: #{pwdstore.username}"
|
20
|
+
}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
out.sort!
|
25
|
+
out.each{ |e| puts e }
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'lockr/action/aes'
|
2
|
+
require 'lockr/pwdstore'
|
3
|
+
|
4
|
+
class RemoveAction < AesAction
|
5
|
+
|
6
|
+
def initialize(id,username,keyfile,vault)
|
7
|
+
keyfilehash = calculate_hash( keyfile)
|
8
|
+
pwd_directory = load_from_vault( vault)
|
9
|
+
|
10
|
+
unless pwd_directory.has_key?( id)
|
11
|
+
puts "Id '#{id}' not found"
|
12
|
+
exit 20
|
13
|
+
end
|
14
|
+
|
15
|
+
pwd_directory_id = YAML::load(decrypt( pwd_directory[id][:enc], keyfilehash, pwd_directory[id][:salt]))
|
16
|
+
|
17
|
+
unless pwd_directory_id.has_key?(username)
|
18
|
+
puts "Username '#{username}' not found for id '#{id}'"
|
19
|
+
exit 21
|
20
|
+
end
|
21
|
+
|
22
|
+
confirm = ask( "Are you sure you want to delete the entry with id '#{id}' and username '#{username}'? (y/n) ") { |q| }
|
23
|
+
unless confirm.downcase == 'y'
|
24
|
+
exit 22
|
25
|
+
end
|
26
|
+
|
27
|
+
pwd_directory_id.delete( username)
|
28
|
+
|
29
|
+
if ( pwd_directory_id.size == 0 )
|
30
|
+
pwd_directory.delete( id)
|
31
|
+
else
|
32
|
+
pwd_directory[id] = {}
|
33
|
+
pwd_directory[id][:enc], pwd_directory[id][:salt] = encrypt( pwd_directory_id.to_yaml, keyfilehash)
|
34
|
+
end
|
35
|
+
|
36
|
+
save_to_vault( pwd_directory, vault)
|
37
|
+
puts "Entry removed"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'lockr/action/aes'
|
2
|
+
|
3
|
+
class ShowAction < AesAction
|
4
|
+
|
5
|
+
def initialize(id,username,keyfile, vault)
|
6
|
+
keyfilehash = calculate_hash( keyfile)
|
7
|
+
|
8
|
+
pwd_directory = load_from_vault( vault)
|
9
|
+
|
10
|
+
unless pwd_directory.has_key?( id)
|
11
|
+
puts "Id '#{id}' not found"
|
12
|
+
exit 10
|
13
|
+
end
|
14
|
+
|
15
|
+
pwd_directory_id = YAML::load(decrypt( pwd_directory[id][:enc], keyfilehash, pwd_directory[id][:salt]))
|
16
|
+
|
17
|
+
if username.nil?
|
18
|
+
unless pwd_directory_id.length == 1
|
19
|
+
puts "More than one username for id '#{id}'. Please provide a username!"
|
20
|
+
exit 13
|
21
|
+
end
|
22
|
+
|
23
|
+
key = pwd_directory_id.keys[0]
|
24
|
+
store = pwd_directory_id[key]
|
25
|
+
else
|
26
|
+
unless pwd_directory_id.has_key?(username)
|
27
|
+
puts "Username '#{username}' not found for id '#{id}'"
|
28
|
+
exit 11
|
29
|
+
end
|
30
|
+
|
31
|
+
store = pwd_directory_id[username]
|
32
|
+
end
|
33
|
+
|
34
|
+
say("Password found")
|
35
|
+
say("ID '<%= color('#{store.id}', :blue) %>', URL '<%= color('#{store.url}', :blue) %>'")
|
36
|
+
say("User '<%= color('#{store.username}', :blue) %>'")
|
37
|
+
say("Password: <%= color('#{store.password}', :green) %>")
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Aes
|
5
|
+
# encrypt the string with AES 256-bit CBC encryption.
|
6
|
+
# the key and iv are calculated by the derive_key_iv
|
7
|
+
# method using the provided password.
|
8
|
+
#
|
9
|
+
# returns encrypted_string, salt
|
10
|
+
def encrypt( string, pass)
|
11
|
+
salt = SecureRandom.random_bytes(16)
|
12
|
+
key, iv = derive_key_iv( pass, salt)
|
13
|
+
|
14
|
+
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
15
|
+
cipher.encrypt
|
16
|
+
cipher.key = key
|
17
|
+
cipher.iv = iv
|
18
|
+
[cipher.update(string) + cipher.final, salt]
|
19
|
+
end
|
20
|
+
|
21
|
+
# decrypt the string with AES 256-bit CBC encryption.
|
22
|
+
# the key and iv are calculated by the derive_key_iv
|
23
|
+
# method using the provided password.
|
24
|
+
def decrypt( string, pass, salt)
|
25
|
+
key, iv = derive_key_iv( pass, salt)
|
26
|
+
|
27
|
+
decipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
28
|
+
decipher.decrypt
|
29
|
+
decipher.key = key
|
30
|
+
decipher.iv = iv
|
31
|
+
|
32
|
+
decipher.update( string) + decipher.final
|
33
|
+
end
|
34
|
+
|
35
|
+
# derive a key and initial vector from the password thru
|
36
|
+
# the use of PKCS5 pbkdf2 key derivation function.
|
37
|
+
def derive_key_iv( pass, salt)
|
38
|
+
key = OpenSSL::PKCS5::pbkdf2_hmac_sha1( pass, salt, 4096, 32)
|
39
|
+
iv = OpenSSL::PKCS5::pbkdf2_hmac_sha1( pass, salt, 4096, 16)
|
40
|
+
|
41
|
+
[key, iv]
|
42
|
+
end
|
43
|
+
end
|
data/lib/lockr/pwdgen.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
class PasswordGenerator
|
4
|
+
|
5
|
+
def generate( params)
|
6
|
+
pwd = []
|
7
|
+
|
8
|
+
# create x random characters
|
9
|
+
params.to_i.times do
|
10
|
+
l = SecureRandom.random_number(26)
|
11
|
+
pwd << l
|
12
|
+
end
|
13
|
+
|
14
|
+
# up/downcase with 50% chance
|
15
|
+
pwd.collect!{ |i| (i + 65).chr }.collect!{ |c|
|
16
|
+
if ( SecureRandom.random_number(0) > 0.5)
|
17
|
+
c.downcase
|
18
|
+
else
|
19
|
+
c
|
20
|
+
end
|
21
|
+
}
|
22
|
+
|
23
|
+
pwd.join
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# A password store contains an id of the site the password belongs to,
|
2
|
+
# the username to login and the password.
|
3
|
+
|
4
|
+
class PasswordStore
|
5
|
+
attr_accessor :id,:url,:username,:password
|
6
|
+
|
7
|
+
def initialize(id,url,username,pwd)
|
8
|
+
@id = id
|
9
|
+
@url = url
|
10
|
+
@username = username
|
11
|
+
@password = pwd
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lockr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Marc Doerflinger
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: highline
|
16
|
+
requirement: &70213081483480 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70213081483480
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bundler
|
27
|
+
requirement: &70213081482560 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70213081482560
|
36
|
+
description: Store your passwords AES encrypted in a simple yaml file
|
37
|
+
email: info@byteblues.com
|
38
|
+
executables:
|
39
|
+
- lockr
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- lib/lockr.rb
|
44
|
+
- lib/lockr/action/add.rb
|
45
|
+
- lib/lockr/action/aes.rb
|
46
|
+
- lib/lockr/action/base.rb
|
47
|
+
- lib/lockr/action/list.rb
|
48
|
+
- lib/lockr/action/remove.rb
|
49
|
+
- lib/lockr/action/show.rb
|
50
|
+
- lib/lockr/encryption/aes.rb
|
51
|
+
- lib/lockr/pwdgen.rb
|
52
|
+
- lib/lockr/pwdstore.rb
|
53
|
+
- !binary |-
|
54
|
+
YmluL2xvY2ty
|
55
|
+
homepage: http://lockr.byteblues.com/
|
56
|
+
licenses: []
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.8.10
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Safe password storage
|
79
|
+
test_files: []
|