lockr 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/lockr ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'lockr'
4
+ lockr = Lockr.new()
5
+ lockr.run()
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,5 @@
1
+ require 'lockr/action/base'
2
+
3
+ class AesAction < BaseAction
4
+ include Aes
5
+ 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
@@ -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: []