lockr 0.2.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.
- 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: []
|