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.
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] https://ruby-doc.org/stdlib/libdoc/digest/rdoc/index.html
[2] https://crypt.rubyforge.org/
[3] https://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: https://emacs-fu.blogspot.com/2011/02/keeping-your-secrets-secret.html
"Since version 23, Emacs includes a package called EasyPG (an interface to GnuPG: https://epg.sourceforge.jp/) which makes this seamless - just make sure that you have GnuPG (https://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.