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 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: []