Class: Yak

Inherits:
Object
  • Object
show all
Defined in:
lib/yak.rb

Overview

Yak is a simple command line app to store and retrieve passwords securely. Retrieved passwords get copied to the clipboard by default. Config can be set in ~/.yakrc:

:session: 30

Session is the length of time in seconds that Yak will remember the master password. If using sessions is not desired, set:

:session: false

To always set the password by default, use:

:password: plain_text_password

To turn off password confirmation prompts:

:confirm_prompt: false

Constant Summary collapse

VERSION =
"1.0.2"
DEFAULT_CONFIG =
{:session => 30}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user, options = {}) ⇒ Yak

Create a new Yak instance for a given user:

Yak.new "my_user"
Yak.new "my_user", :session => 10
Yak.new `whoami`.chomp, :session => false


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/yak.rb', line 187

def initialize user, options={}
  @user     = user
  @input    = HighLine.new $stdin, $stderr

  @confirm_prompt = true
  @confirm_prompt = options[:confirm_prompt] if
    options.has_key? :confirm_prompt

  @yak_dir = File.expand_path "~#{@user}/.yak"
  FileUtils.mkdir @yak_dir unless File.directory? @yak_dir

  @pid_file      = File.join @yak_dir, "pid"
  @password_file = File.join @yak_dir, "password"
  @data_file     = File.join @yak_dir, "data"

  @session_pid = nil
  @session_pid = File.read(@pid_file).to_i if File.file? @pid_file

  @password = get_password options[:password]

  @cipher = OpenSSL::Cipher::Cipher.new "aes-256-cbc"

  @session_length = options.has_key?(:session) ? options[:session] : 30

  connect_data
  start_session
end

Instance Attribute Details

#dataObject (readonly)

Returns the value of attribute data.



179
180
181
# File 'lib/yak.rb', line 179

def data
  @data
end

#userObject (readonly)

Returns the value of attribute user.



179
180
181
# File 'lib/yak.rb', line 179

def user
  @user
end

Class Method Details

.list(yak, name = nil) ⇒ Object



86
87
88
89
90
91
92
# File 'lib/yak.rb', line 86

def self.list yak, name=nil
  key_regex = /#{name || ".+"}/

  yak.data.each do |key, value|
    $stdout << "#{key}: #{value}\n" if key =~ key_regex
  end
end

.load_configObject

Load the ~/.yakrc file and return. Creates ~/.yakrc with the default config if missing.



57
58
59
60
61
62
63
64
65
66
# File 'lib/yak.rb', line 57

def self.load_config
  config_file = File.expand_path "~/.yakrc"

  if !File.file?(config_file)
    File.open(config_file, "w+"){|f| f.write DEFAULT_CONFIG.to_yaml }
    $stderr << "Created Yak config file #{config_file}\n"
  end

  YAML.load_file config_file
end

.new_password(yak, value = nil) ⇒ Object



95
96
97
98
99
# File 'lib/yak.rb', line 95

def self.new_password yak, value=nil
  yak.new_password value
  yak.write_data
  yak.start_session
end

.parse_args(argv) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/yak.rb', line 121

def self.parse_args argv
  options = {}

  opts = OptionParser.new do |opt|
    opt.program_name = File.basename $0
    opt.version = VERSION
    opt.release = nil

    opt.banner = <<-EOF
#{opt.program_name} is a simple app to store and retrieve passwords securely.
Retrieved passwords get copied to the clipboard by default.

Usage:
  #{opt.program_name} [options] [key] [password]

Examples:
  #{opt.program_name} -a gmail [password]
  #{opt.program_name} gmail
  #{opt.program_name} -r gmail
  #{opt.program_name} --list
  
Options:
    EOF

    opt.on('-a', '--add KEY',
           'Add a new password for a given key') do |key|
      options[:action] = :store
      options[:key]    = key
    end

    opt.on('-r', '--remove KEY',
           'Remove the password for a given key') do |key|
      options[:action] = :remove
      options[:key]    = key
    end

    opt.on('-l', '--list [REGEX]',
           'List key/password pairs to the stdout') do |key|
      options[:action] = :list
      options[:key]    = key
    end

    opt.on('-n', '--new-password',
           'Update the password used for encryption') do |value|
      options[:action] = :new_password
    end
  end

  opts.parse! argv

  options[:action] ||= :retrieve
  options[:key]    ||= argv.shift
  options[:value]  ||= argv.shift

  options
end

.remove(yak, name) ⇒ Object



69
70
71
72
# File 'lib/yak.rb', line 69

def self.remove yak, name
  yak.remove name
  yak.write_data
end

.retrieve(yak, name) ⇒ Object



81
82
83
# File 'lib/yak.rb', line 81

def self.retrieve yak, name
  send_to_clipboard yak.retrieve(name)
end

.run(argv = ARGV) ⇒ Object

Run Yak with argv:

Yak.run %w{key}
Yak.run %w{--add key}
...


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/yak.rb', line 36

def self.run argv=ARGV
  config = DEFAULT_CONFIG.merge load_config

  options = parse_args argv

  yak = new `whoami`.chomp, config

  args = [options[:action], yak, options[:key], options[:value]].compact

  self.send(*args)

rescue OpenSSL::CipherError => e
  $stderr << "Bad password.\n"
  exit 1
end

.send_to_clipboard(string) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/yak.rb', line 102

def self.send_to_clipboard string
  copy_cmd = case RUBY_PLATFORM
             when /darwin/
               "echo -n \"#{string}\" | pbcopy"
             when /linux/
               "echo -n \"#{string}\" | xclip"
             when /cigwin/
               "echo -n \"#{string}\" | putclip"
             when /(win|mingw)/
               "echo \"#{string}\" | clip"
             else
               $stderr << "No clipboad cmd for platform #{RUBY_PLATFORM}\n"
               exit 1
             end

  Session::Bash.new.execute copy_cmd
end

.store(yak, name, value = nil) ⇒ Object



75
76
77
78
# File 'lib/yak.rb', line 75

def self.store yak, name, value=nil
  yak.store name, value
  yak.write_data
end

Instance Method Details

#connect_dataObject

Loads and decrypts the data file into the @data attribute.



282
283
284
285
286
287
288
289
290
# File 'lib/yak.rb', line 282

def connect_data
  @data = if File.file? @data_file
            data = ""
            File.open(@data_file, "rb"){|f| data << f.read }
            YAML.load decrypt(data)
          else
            {}
          end
end

#decrypt(string, password = @password) ⇒ Object

Decrypt a string with a given password.



321
322
323
324
325
# File 'lib/yak.rb', line 321

def decrypt string, password=@password
  @cipher.decrypt
  @cipher.key = password
  get_cypher_out string
end

#encrypt(string, password = @password) ⇒ Object

Encrypt a string with a given password.



331
332
333
334
335
# File 'lib/yak.rb', line 331

def encrypt string, password=@password
  @cipher.encrypt
  @cipher.key = password
  get_cypher_out string
end

#end_sessionObject

Stop a session.



239
240
241
242
243
# File 'lib/yak.rb', line 239

def end_session
  return unless @session_pid
  Process.kill 9, @session_pid rescue false
  FileUtils.rm_f [@password_file, @pid_file]
end

#get_password(plain_password = nil) ⇒ Object

Get a password from either the password file or by prompting the user if a password file is unavailable. Returns a sha1 of the password passed as an arg.



259
260
261
262
263
264
265
266
# File 'lib/yak.rb', line 259

def get_password plain_password=nil
  password   = File.read @password_file if File.file? @password_file

  password ||=
    Digest::SHA1.hexdigest(plain_password || request_password("Yak Password"))

  password
end

#has_session?Boolean

Check if a session is active.

Returns:

  • (Boolean)


249
250
251
# File 'lib/yak.rb', line 249

def has_session?
  Process.kill(0, @session_pid) && @session_pid rescue false
end

#new_password(password = nil) ⇒ Object

Prompt the user for a new password (replacing and old one). Prompts for password confirmation as well.



273
274
275
276
# File 'lib/yak.rb', line 273

def new_password password=nil
  password ||= request_new_password "New Password"
  @password  = Digest::SHA1.hexdigest password if password
end

#remove(name) ⇒ Object

Remove a key/value pair.



296
297
298
# File 'lib/yak.rb', line 296

def remove name
  @data.delete(name)
end

#retrieve(name) ⇒ Object

Retrieve a value for a given key.



304
305
306
# File 'lib/yak.rb', line 304

def retrieve name
  @data[name]
end

#start_sessionObject

Start a new session during which Yak will remember the user’s password.



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/yak.rb', line 219

def start_session
  return unless @session_length

  end_session if has_session?

  pid = fork do
    sleep @session_length
    FileUtils.rm_f [@password_file, @pid_file]
  end

  File.open(@pid_file,      "w+"){|f| f.write pid }
  File.open(@password_file, "w+"){|f| f.write @password }

  Process.detach pid
end

#store(name, value = nil) ⇒ Object

Add a key/value pair. If no value is passed, will prompt the user for one.



312
313
314
315
# File 'lib/yak.rb', line 312

def store name, value=nil
  value ||= request_new_password "'#{name}' Password"
  @data[name] = value
end

#write_data(password = @password) ⇒ Object

Encrypt and write the Yak data back to the data file.



341
342
343
344
# File 'lib/yak.rb', line 341

def write_data password=@password
  data = encrypt @data.to_yaml, password
  File.open(@data_file, "w+"){|f| f.write data}
end