Working with Ruby by Jan Lelis

Tutorial: Build your own password safe with Ruby!

There are many implementations of password managers/safes out there. But lots of them are black boxes, either because they are not open source, or because they have to much features and it gets complicated to understand the source (which is most likely not written in a happy programming language). You don’t know, what really happens with your passwords. So…

Do it yourself!
Do it with Ruby!
Do it in less than 250 lines ;)

Although this tutorial is for people who want to learn how to use Ruby, you should be familiar with the Ruby basics.

The article is divided into four/five phases, each with a code snippet and some explanations about some lines that might not be perspicuous at first glance.

Phase 0: What is it about?

What should be the purpose of the program? These are my thoughts

  • It should be a little command line utility, called pws
  • It should store many passwords for you, and protect them with a master password
  • It should encrypt the password store
  • It should be easy to use and especially be useful for every-day-use
  • It should stay simple!

Phase I: Encryption

Let’s dive into it with the most exciting part: Encryption (because it’s an important part). A quick search on the net reveals how to. Let’s modernize and refactor it into a handy, small module:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
require 'openssl'

class PasswordSafe
  VERSION = '0.0.1'.freeze
end

class << Encryptor = Module.new
  CIPHER = 'AES256'

  def decrypt( data, pwhash )
    crypt :decrypt, data, pwhash
  end

  def encrypt( data, pwhash )
    crypt :encrypt, data, pwhash
  end

  def hash( plaintext )
    OpenSSL::Digest::SHA512.new( plaintext ).digest
  end

  private

  # Encrypts or decrypts the data with the password hash as key
  # NOTE: encryption exceptions do not get caught!
  def crypt( decrypt_or_encrypt, data, pwhash )
    c = OpenSSL::Cipher.new CIPHER
    c.send decrypt_or_encrypt.to_sym
    c.key = pwhash
    c.update( data ) << c.final
  end
end

# Example
if __FILE__ == $0
  a = "data"
  b = Encryptor.hash 'password'
  c = Encryptor.encrypt a, b
  puts 'Encrypted: ' + c.inspect
  d = Encryptor.decrypt c, b
  puts 'Decrypted: ' + d
end

# J-_-L

class << Encryptor = Module.new

Creates a new module and opens its eigenclass. It’s the same like: module Encryptor; class << self

c.send decrypt_or_encrypt.to_sym

send calls the method defined by the symbol given

c.update( data ) << c.final

OpenSSL API

__FILE__ == $0

Returns true, if the file is executed directly

Please note: The final code will use the better encryption cipher method cbc.

Phase II: Save data in a file & basic structure

Now, let’s build a simple PasswordSafe class and integrate the Encryptor.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
require 'openssl'
require 'fileutils'

class PasswordSafe
  VERSION = '0.0.2'.freeze

  def initialize( filename = File.expand_path('~/.pws') )
    @pwfile = filename
    @pwdata = "example data"
    @pwhash = Encryptor.hash 'password'

    access_safe
    read_safe
  end

  private

  # Tries to load and decrypt the password safe from the pwfile
  def read_safe
    pwdata_encrypted = File.read @pwfile
    @pwdata          = Encryptor.decrypt pwdata_encrypted, @pwhash
  end

  # Tries to encrypt and save the password safe into the pwfile
  def write_safe
    pwdata_encrypted = Encryptor.encrypt @pwdata, @pwhash
    File.open( @pwfile, 'w' ){ |f| f.write pwdata_encrypted }
  end
  
  # Checks if the file is accessible or create a new one
  def access_safe
    if !File.file? @pwfile
      puts "No password safe detected, creating one at #@pwfile"
      FileUtils.touch @pwfile
      write_safe
    end
  end

  class << Encryptor = Module.new
    CIPHER = 'AES256'

    def decrypt( data, pwhash )
      crypt :decrypt, data, pwhash
    end

    def encrypt( data, pwhash )
      crypt :encrypt, data, pwhash
    end

    def hash( plaintext )
      OpenSSL::Digest::SHA512.new( plaintext ).digest
    end

    private

    # Encrypts or decrypts the data with the password hash as key
    # NOTE: encryption exceptions do not get caught!
    def crypt( decrypt_or_encrypt, data, pwhash )
      c = OpenSSL::Cipher.new CIPHER
      c.send decrypt_or_encrypt.to_sym
      c.key = pwhash
      c.update( data ) << c.final
    end
  end
end

if __FILE__ == $0 # test whether it works :)
  pws = PasswordSafe.new 'p2test'
  print 'Enter data to encrypt: '
  pws.instance_variable_set :@pwdata, gets.chop
  pws.send :write_safe

  puts "In safe: " +
    (File.read pws.instance_variable_get :@pwfile).inspect

  pws = PasswordSafe.new 'p2test'
  pws.send :read_safe
  puts "Read from safe: " + pws.instance_variable_get(:@pwdata)
end

# J-_-L

def initialize( filename = File.expand_path('~/.pws') )

A new password safe is associated with a password file

pwdata_encrypted = File.read @pwfile

Reads the file into a string

File.open( @pwfile, 'w' ){ |f| f.write pwdata_encrypted }

Writes the string into a file

FileUtils.touch @pwfile

Creates an empty file just like the unix tool

pws.instance_variable_set :@pwdata, gets.chop

This is an example of the flexibility of Ruby: Access to all private variables

Also note: The Encryptor module is now within the PasswordSafe scope.

Phase III: Data structure & public api

This phase completes the basic functionality:

  • The passwords get saved in a hash of the Entry data structure and this hash gets saved in a file using the Marshal class for serializing
  • When retrieving a password, it’s copied to the clipboard
  • Furthermore, the zucker gem is used to write some pieces of code more cleanly
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
require 'rubygems' if RUBY_VERSION[2] == ?8

require 'openssl'
require 'fileutils'
require 'clipboard'         # gem install clipboard
require 'zucker/alias_for'  # gem install zucker
require 'zucker/egonil'
require 'zucker/kernel'

class PasswordSafe
  VERSION = "0.0.3".freeze

  Entry = Struct.new :description, :password

  def initialize( filename = File.expand_path('~/.pws') )
    @pwfile = filename

    access_safe
    read_safe
  end

  def add(key, description = nil, password = nil)
    @pwdata[key]             = Entry.new
    @pwdata[key].password    = password || ask_for_password( "please enter a password for #{key}" )
    @pwdata[key].description = description
    write_safe
  end
  aliases_for :add, :a, :set, :create, :update, :[]= # using zucker/alias_for

  def get(key)
    if pw_plaintext = @pwdata[key] && @pwdata[key].password
      Clipboard.copy pw_plaintext
      puts "The password has been copied to your clipboard"
    else
      puts "No password entry found for #{key}"
    end
  end
  aliases_for :get, :g, :entry, :[]

  def remove(key)
    if @pwdata.delete key
      puts "#{key} has been removed"
    else
      puts "Nothing removed"
    end
  end
  aliases_for :remove, :r, :delete

  def show
    puts "Available passwords \n" +

    if @pwdata.empty? 
      '  (none)'
    else
      @pwdata.map{ |key, pwentry|
        "  #{key}" + if pwentry.description then ": #{pwentry.description}" else '' end
      }*"\n" 
    end
  end
  aliases_for :show, :s, :list

  def description(*keys)
    keys.each{ |key|
      puts (@pwdata[key] && @pwdata[key].description) || key
    }
  end

  def master
    @pwhash = Encryptor.hash ask_for_password 'please enter a new master password'
    write_safe
  end
  aliases_for :master, :m

  private

  # Tries to load and decrypt the password safe from the pwfile
  def read_safe
    pwdata_encrypted = File.read @pwfile
    pwdata_dump      = Encryptor.decrypt( pwdata_encrypted, @pwhash )
    @pwdata          = Marshal.load(pwdata_dump) || {}
  end

  # Tries to encrypt and save the password safe into the pwfile
  def write_safe
    pwdata_dump      = Marshal.dump @pwdata || {}
    pwdata_encrypted = Encryptor.encrypt pwdata_dump, @pwhash
    File.open( @pwfile, 'w' ){ |f| f.write pwdata_encrypted }
  end
  
  # Checks if the file is accessible or create a new one
  def access_safe
    if !File.file? @pwfile
      puts "No password safe detected, creating one at #@pwfile"
      FileUtils.touch @pwfile
      @pwhash = Encryptor.hash ask_for_password 'please enter a new master password'
      write_safe
    else
      @pwhash = Encryptor.hash ask_for_password 'master password'
    end
  end

  def ask_for_password(prompt = 'new password')
    print "#{prompt}: ".capitalize
    system 'stty -echo'                    # no more terminal output
    pw_plaintext = ($stdin.gets||'').chop  # gets without $stdin would mistakenly read_safe from ARGV
    system 'stty echo'                     # restore terminal output
    puts

    pw_plaintext
  end

  class << Encryptor = Module.new
    CIPHER = 'AES256'

    def decrypt( data, pwhash )
      crypt :decrypt, data, pwhash
    end

    def encrypt( data, pwhash )
      crypt :encrypt, data, pwhash
    end

    def hash( plaintext )
      OpenSSL::Digest::SHA512.new( plaintext ).digest
    end

    private

    # Encrypts or decrypts the data with the password hash as key
    # NOTE: encryption exceptions do not get caught!
    def crypt( decrypt_or_encrypt, data, pwhash )
      c = OpenSSL::Cipher.new CIPHER
      c.send decrypt_or_encrypt.to_sym
      c.key = pwhash
      c.update( data ) << c.final
    end
  end
end

if standalone? # using zucker/kernel (instead of __FILE__ == $0)
  pws = PasswordSafe.new 'p3test'
  pws.send $*.shift.to_sym, *$*
end

# J-_-L

require 'rubygems' if RUBY_VERSION[2] == ?8

In 1.8, you need to require rubygems, in 1.9 gem paths are automatically added to your load path

Entry = Struct.new :description, :password

The Struct class is a quick way to generate classes for objects with some simple accessors

pwdata[key].password = password || ask_for_password

Assigns the value of password, unless it’s nil: then ask_for_password gets called instead

aliases_for :add, :a, :[]=, :set, :create

This lines uses my zucker gem to create many aliases for the add method. Without the gem, you have to write alias set add; alias a add; #...

@pwdata && @pwdata[key].password

Only return the description if the entry is present. You could also use the egonil.

pws.send $*.shift.to_sym, *$*

$* contains the command line arguments. We send it to the pws method named by the first argument and pass the remaining ones as method arguments.

Phase IV: Usability & bonus features

In this last step, we add some useful output messages for the user as well as some convenience features:

  • The password gets copied to the clipboard only for some seconds
  • Before saving, some redundant data is added to avoid known-plaintext attacks
  • More helper methods, e.g. generating a random password

#!/usr/bin/env ruby

This is the shebang, so you can now execute the file directly. But you have to make it executable, e.g. with: chmod +x pws

class NoAccess < StandardError; end

Our own ErrorClass that we can present the user instead of some OpenSSL ones

(1..length).map{ chars.sample }.join

This line generates a random password

$*.shift[/^-{0,2}(.*)$/, 1].to_sym

The regex is applied to the string and the “first group” substring is sliced: Allows calling the action with - and -- (because some people are used to it).

if PasswordSafe.public_instance_methods(false).include?(
      if RubyVersion.is?(1.8) then action.to_s else action end )

Only allow methods that we have defined ourselves

The output is different on 1.8/1.9, so it uses the zucker/version constant RubyVersion. You could also use the standard RUBY_VERSION constant.

Update

Now using the better cbc version of the aes encryption algorithm.

pws :)

There already is a follow-up announcement on a newer version of pws.

The final code is available on github and as gem.

Creative Commons License

Comments

drew | November 01, 2010

If you are using ruby 1.8.7 or later you no longer need to require fileutils.

chris | November 01, 2010

Surely this could have been done without opening the eigenclass? It's hardly simple ruby code.

Rob Gleeson | November 02, 2010

unnecessarily complex -- especially how you open the singleton class. I think you're trying to be fancy at the cost of confusing your supposed target audience.

The best code is understandable code, and I think you went out of your way to complicate the code in your examples.

J-_-L | November 02, 2010

@drew: Thanks for this hint<br/>@chris and Rob Gleeson: If I didn't open the eigenclass, I would have to prefix every method in the module with <code>self.</code> - it's common behaviour to open the eigenclass instead. I've just written it a different way to avoid the double nesting.

Jarmo Pertman | November 02, 2010

Agreed that all above was overly complicated. Using openssl, which is very poorly documented in Ruby is a complex step to do itself. I'd use Digest[1] and Crypt[2] instead for that task.

Also, asking for the password you could have used Highline [3] #ask instead of all that manual coding.

One good thing about Ruby is that "there's probably a gem for that" :)

And i'm pretty sure that the syntax "module Encryptor; class << self" is used a lot more than the syntax showed by you.

[1] http://ruby-doc.org/stdlib/libdoc/digest/rdoc/index.html
[2] http://crypt.rubyforge.org/
[3] http://highline.rubyforge.org/

J-_-L | November 02, 2010

Crypt is indeed better documented, but OpenSSL works fine as well (although it's true, the OpenSSL api is not very intuitive). I had taken a look at HighLine, but I didn't like the api, so I went with the two "manual" lines of code.

Of course, the <code>module Enryptor; class << self</code> variant is used more often, but would it have made sense in this case? It adds an unneeded level of nesting. Whether you open the eigenclass with <code>class << self</code> or with <code>class << Encryptor</code> is not a big difference in my view.

Nevertheless, thanks for the feedback ;)

nimai | November 03, 2010

I like it. Always good to read through other peoples code. I learnt a lot, regardless of what rob and chris have said. I don't think it's unnecessarily complex.

seancribbs | November 03, 2010

The easier pattern for "singleton" modules is simply to make the first line be 'extend self'. Then you need not have that eigenclass confusion, nor create an anonymous module.

module Encryptor; extend self; # etc

Dave Sailer | March 19, 2011

For emacs users, I found this at emacs-fu:

GnuPG: keeping your secrets secret: http://emacs-fu.blogspot.com/2011/02/keeping-your-secrets-secret.html

"Since version 23, Emacs includes a package called EasyPG (an interface to GnuPG: http://epg.sourceforge.jp/) which makes this seamless - just make sure that you have GnuPG (http://www.gnupg.org/) installed.

"The only thing you need to do is adding the .gpg -extension to your files, and EasyPG will automatically encrypt/decrypt them when writing/reading.

"To create an encrypted file, simply visit (C-x C-f) a file with a name like myfile.txt.gpg; emacs opens this just like any file. When you want to save the file, emacs will ask you for a password, and with this same password, you can open it again."

I'm on Ubuntu 10.04, with emacs23, and found that everything was already installed. All I had to do was use it.

seo | May 26, 2015

Hello Web Admin, I noticed that your On-Page SEO is is missing a few factors, for one you do not use all three H tags in your post, also I notice that you are not using bold or italics properly in your SEO optimization. On-Page SEO means more now than ever since the new Google update: Panda. No longer are backlinks and simply pinging or sending out a RSS feed the key to getting Google PageRank or Alexa Rankings, You now NEED On-Page SEO. So what is good On-Page SEO?First your keyword must appear in the title.Then it must appear in the URL.You have to optimize your keyword and make sure that it has a nice keyword density of 3-5% in your article with relevant LSI (Latent Semantic Indexing). Then you should spread all H1,H2,H3 tags in your article.Your Keyword should appear in your first paragraph and in the last sentence of the page. You should have relevant usage of Bold and italics of your keyword.There should be one internal link to a page on your blog and you should have one image with an alt tag that has your keyword....wait there's even more Now what if i told you there was a simple Wordpress plugin that does all the On-Page SEO, and automatically for you? That's right AUTOMATICALLY, just watch this 4minute video for more information at. <a href="http://www.SEORankingLinks.com">Seo Plugin</a>
seo http://www.SEORankingLinks.com/

seo plugin | May 30, 2015

Hello Web Admin, I noticed that your On-Page SEO is is missing a few factors, for one you do not use all three H tags in your post, also I notice that you are not using bold or italics properly in your SEO optimization. On-Page SEO means more now than ever since the new Google update: Panda. No longer are backlinks and simply pinging or sending out a RSS feed the key to getting Google PageRank or Alexa Rankings, You now NEED On-Page SEO. So what is good On-Page SEO?First your keyword must appear in the title.Then it must appear in the URL.You have to optimize your keyword and make sure that it has a nice keyword density of 3-5% in your article with relevant LSI (Latent Semantic Indexing). Then you should spread all H1,H2,H3 tags in your article.Your Keyword should appear in your first paragraph and in the last sentence of the page. You should have relevant usage of Bold and italics of your keyword.There should be one internal link to a page on your blog and you should have one image with an alt tag that has your keyword....wait there's even more Now what if i told you there was a simple Wordpress plugin that does all the On-Page SEO, and automatically for you? That's right AUTOMATICALLY, just watch this 4minute video for more information at. <a href="http://www.SEORankingLinks.com">Seo Plugin</a>
[url=http://www.SEORankingLinks.com/]seo plugin[/url]

Filter Cloth | June 06, 2015

The company founded in 1985, has total assets of RMB1.52 billion, occupies a total area of 800,000 square meters, and employs 3,000 staff members, including 98 senior engineers and technicians and 319 mid-level engineers and technicians.
Filter Cloth http://www.hbfiltercloth.com/

You? | July 02, 2015