HippoMan
HippoMan

Reputation: 2328

ruby: create MD5 checksum with salt?

I'm trying to create an MD5 checksum in ruby with salt, but I cannot find any way to do this using the standard digest/md5 package.

I know I can do this:

require 'digest/md5'
checksum = '$1$' + (Digest::MD5.new << plaintext).to_s

However, there doesn't appear to be any way to specify salt for this MD5 checksum generation using digest, and I haven't found any other package I could use for this in ruby.

Is this even possible in ruby?

Upvotes: 0

Views: 1405

Answers (3)

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84433

Creating/Validating *nix Style MD5 Entries with Salt

If you're trying to manage *nix system passwords, you're better off just shelling out to system utilities rather than building your own. However, if you want to generate or validate salted passwords with only Ruby core or standard library capabilities, you certainly can.

A computed MD5 password with salt is generally stored in a flat-file database (e.g. /etc/shadow) like this, where $ is the field separator:

$1$salt$hashed_pw

Note that the first two fields are stored in cleartext, because they're needed to rebuild and hash the correct string when presented with only the password to validate. As a result, you need to treat the salt as a separate variable from the plaintext password, although the salt is included with the password when hashed.

If you don't have a character limitation imposed by your utilites, one way to generate a strong salt is to use SecureRandom#uuid to generate a UUIDv4 value. For example:

require 'securerandom'

salt = SecureRandom.uuid
#=> "c05280ef-151c-4ebc-83c6-f5f0906f89c2"

You can then invoke your MD5 hash on salt + pw or pw + salt depending on your application's password implementation. For example:

require 'digest/md5'

MD5_STR_FMT = '$1$%s$%s'.freeze

salt = 'c05280ef-151c-4ebc-83c6-f5f0906f89c2'
pw   = 'plaintext password gathered securely'

pw_digest = Digest::MD5.new << salt + pw
pw_entry  = MD5_STR_FMT % [salt, pw_digest]
#=> "$1$c05280ef-151c-4ebc-83c6-f5f0906f89c2$87dcc23c0008e45526e474d0364e4aa5"

You then store pw_entry in your password database file, and then parse out the salt to prepend to the offered password when you recompute the hash during authentication. For example:

require 'digest/md5'

# this is how we'll validate a password from user
# input against an entry from the password database
def valid_pw? pw, salt, hashed_pw
  pw_digest = Digest::MD5.new << salt + pw
  pw_digest.to_s.eql? hashed_pw.to_s
end

# extract salt and password from a database entry
def parse_pw_entry str
  str.split(?$).slice -2, 2
end

# get this from your password database in whatever
# way you like
pw_entry = '$1$c05280ef-151c-4ebc-83c6-f5f0906f89c2$87dcc23c0008e45526e474d0364e4aa5'

# for demonstration purposes only; gather password
# securely from user, then perform your validation
['foo', 'plaintext password gathered securely'].map do |pw|
  valid_pw? pw, *parse_pw_entry(pw_entry)
end
#=> [false, true]

Upvotes: 1

HippoMan
HippoMan

Reputation: 2328

I found the following, and it does what I want ...

https://github.com/mogest/unix-crypt

It works like this:

require 'unix_crypt'
checksum = UnixCrypt::MD5.build(plaintext, salt)

This generates the same checksum as is used in /etc/shadow, which is what I want to use it for,

Upvotes: 0

spickermann
spickermann

Reputation: 107097

You can add compute the digest of multiple chunks like this:

require 'digest/md5'

md5 = Digest::MD5.new
md5 << '$1$'
md5 << plaintext

checksum = md5.to_s

Or by string concatenation of the salt and the text in one method call:

salt = '$1$'
checksum = Digest::MD5.hexdigest("#{salt}#{plaintext}")

Upvotes: 0

Related Questions