├── .gitignore ├── Rakefile ├── bin └── mkunixcrypt ├── lib ├── unix_crypt │ ├── des.rb │ ├── md5.rb │ ├── base.rb │ ├── sha.rb │ └── command_line.rb └── unix_crypt.rb ├── unix-crypt.gemspec ├── LICENSE ├── README.rdoc └── test ├── unix_crypt └── test_command_line.rb └── test_unix_crypt.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .*.swp 3 | *.gem 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs << "test" 5 | t.test_files = FileList['test/**/test*.rb'] 6 | t.verbose = true 7 | end 8 | -------------------------------------------------------------------------------- /bin/mkunixcrypt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | begin 4 | require 'unix_crypt' 5 | rescue LoadError 6 | require 'rubygems' 7 | require 'unix_crypt' 8 | end 9 | 10 | require 'unix_crypt/command_line' 11 | 12 | begin 13 | UnixCrypt::CommandLine.new(ARGV).encrypt 14 | rescue UnixCrypt::CommandLine::Abort => e 15 | abort e.message 16 | end 17 | -------------------------------------------------------------------------------- /lib/unix_crypt/des.rb: -------------------------------------------------------------------------------- 1 | class UnixCrypt::DES < UnixCrypt::Base 2 | def self.hash(*args) 3 | raise "Unimplemented for DES" 4 | end 5 | 6 | protected 7 | def self.construct_password(password, salt, rounds) 8 | password.crypt(salt) 9 | end 10 | 11 | def self.default_salt_length; 2; end 12 | def self.max_salt_length; 2; end 13 | end 14 | -------------------------------------------------------------------------------- /unix-crypt.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require 'unix_crypt' 5 | 6 | spec = Gem::Specification.new do |s| 7 | s.name = 'unix-crypt' 8 | s.version = UnixCrypt::VERSION 9 | s.summary = "Performs the UNIX crypt(3) algorithm using DES, MD5, SHA256 or SHA512" 10 | s.description = %{Performs the UNIX crypt(3) algorithm using DES (standard 13 character passwords), MD5 (starting with $1$), SHA256 (starting with $5$) and SHA512 (starting with $6$)} 11 | s.files = `git ls-files`.split($\) 12 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 13 | s.executables = ["mkunixcrypt"] 14 | s.require_path = 'lib' 15 | s.author = "Roger Nesbitt" 16 | s.email = "roger@seriousorange.com" 17 | s.homepage = "https://github.com/mogest/unix-crypt" 18 | s.license = "0BSD" 19 | end 20 | -------------------------------------------------------------------------------- /lib/unix_crypt.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | require 'securerandom' 3 | 4 | module UnixCrypt 5 | VERSION = "1.3.1" 6 | 7 | Error = Class.new(StandardError) 8 | SaltTooLongError = Class.new(Error) 9 | 10 | def self.valid?(password, string) 11 | # Handle the original DES-based crypt(3) 12 | return password.crypt(string) == string if string.length == 13 13 | 14 | # All other types of password follow a standard format 15 | return false unless m = string.match(/\A\$([156])\$(?:rounds=(\d+)\$)?(.+)\$(.+)/) 16 | 17 | hash = IDENTIFIER_MAPPINGS[m[1]].hash(password, m[3], m[2] && m[2].to_i) 18 | hash == m[4] 19 | end 20 | end 21 | 22 | require 'unix_crypt/base' 23 | require 'unix_crypt/des' 24 | require 'unix_crypt/md5' 25 | require 'unix_crypt/sha' 26 | 27 | UnixCrypt::IDENTIFIER_MAPPINGS = { 28 | '1' => UnixCrypt::MD5, 29 | '5' => UnixCrypt::SHA256, 30 | '6' => UnixCrypt::SHA512 31 | } 32 | -------------------------------------------------------------------------------- /lib/unix_crypt/md5.rb: -------------------------------------------------------------------------------- 1 | class UnixCrypt::MD5 < UnixCrypt::Base 2 | protected 3 | def self.digest; Digest::MD5; end 4 | def self.length; 16; end 5 | def self.default_salt_length; 8; end 6 | def self.max_salt_length; 8; end 7 | def self.identifier; 1; end 8 | 9 | def self.byte_indexes 10 | [[0, 6, 12], [1, 7, 13], [2, 8, 14], [3, 9, 15], [4, 10, 5], [nil, nil, 11]] 11 | end 12 | 13 | def self.internal_hash(password, salt, ignored = nil) 14 | salt = salt[0..7] 15 | 16 | b = digest.digest("#{password}#{salt}#{password}") 17 | a_string = "#{password}$1$#{salt}#{b * (password.length/length)}#{b[0...password.length % length]}" 18 | 19 | password_length = password.length 20 | while password_length > 0 21 | a_string += (password_length & 1 != 0) ? "\x0" : password[0].chr 22 | password_length >>= 1 23 | end 24 | 25 | input = digest.digest(a_string) 26 | 27 | 1000.times do |index| 28 | c_string = ((index & 1 != 0) ? password : input) 29 | c_string += salt unless index % 3 == 0 30 | c_string += password unless index % 7 == 0 31 | c_string += ((index & 1 != 0) ? input : password) 32 | input = digest.digest(c_string) 33 | end 34 | 35 | input 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Roger Nesbitt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | Neither the name of the unix-crypt nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /lib/unix_crypt/base.rb: -------------------------------------------------------------------------------- 1 | class UnixCrypt::Base 2 | def self.build(password, salt = nil, rounds = nil) 3 | salt ||= generate_salt 4 | if salt.length > max_salt_length 5 | raise UnixCrypt::SaltTooLongError, "Salts longer than #{max_salt_length} characters are not permitted" 6 | end 7 | 8 | construct_password(password, salt, rounds) 9 | end 10 | 11 | def self.hash(password, salt, rounds = nil) 12 | bit_specified_base64encode internal_hash(prepare_password(password), salt, rounds) 13 | end 14 | 15 | def self.generate_salt 16 | # Generates a random salt using the same character set as the base64 encoding 17 | # used by the hash encoder. 18 | SecureRandom.base64((default_salt_length * 6 / 8.0).ceil).tr("+", ".")[0...default_salt_length] 19 | end 20 | 21 | protected 22 | def self.construct_password(password, salt, rounds) 23 | "$#{identifier}$#{rounds_marker rounds}#{salt}$#{hash(password, salt, rounds)}" 24 | end 25 | 26 | def self.bit_specified_base64encode(input) 27 | b64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 28 | input = input.bytes.to_a 29 | output = "" 30 | byte_indexes.each do |i3, i2, i1| 31 | b1, b2, b3 = i1 && input[i1] || 0, i2 && input[i2] || 0, i3 && input[i3] || 0 32 | output << 33 | b64[ b1 & 0b00111111] << 34 | b64[((b1 & 0b11000000) >> 6) | 35 | ((b2 & 0b00001111) << 2)] << 36 | b64[((b2 & 0b11110000) >> 4) | 37 | ((b3 & 0b00000011) << 4)] << 38 | b64[ (b3 & 0b11111100) >> 2] 39 | end 40 | 41 | remainder = 3 - (length % 3) 42 | remainder = 0 if remainder == 3 43 | output[0..-1-remainder] 44 | end 45 | 46 | def self.prepare_password(password) 47 | # For Ruby 1.9+, convert the password to UTF-8, then treat that new string 48 | # as binary for the digest methods. 49 | if password.respond_to?(:encode) 50 | password = password.encode("UTF-8") 51 | password.force_encoding("ASCII-8BIT") 52 | end 53 | 54 | password 55 | end 56 | 57 | def self.rounds_marker(rounds) 58 | nil 59 | end 60 | end 61 | 62 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = unix-crypt 2 | 3 | == Description 4 | 5 | unix-crypt creates and checks passwords that you'd normally find in an /etc/shadow file on your UNIX box. 6 | 7 | It's written entirely in Ruby and has no external dependencies. 8 | 9 | It handles: 10 | * DES passwords (the standard 13 character password with a 2 character salt) 11 | * MD5 passwords (starting with $1$) 12 | * SHA256 passwords (starting with $5$) 13 | * SHA512 passwords (starting with $6$) 14 | 15 | This library is compatible with Ruby 1.8.7 and above. Tested on Ruby 2.0.0p353. 16 | 17 | == Installation 18 | 19 | gem install unix-crypt 20 | 21 | == Using the command line tool 22 | 23 | An executable named +mkunixcrypt+ allows you to generate passwords from the command line. 24 | 25 | Usage: mkunixcrypt [options] 26 | Encrypts password using the unix-crypt gem 27 | 28 | Options: 29 | -h, --hash [HASH] Set hash algorithm [SHA512 (default), SHA256, MD5, DES] 30 | -p, --password [PASSWORD] Provide password on command line (insecure!) 31 | -s, --salt [SALT] Provide hash salt 32 | -r, --rounds [ROUNDS] Set number of hashing rounds (SHA256/SHA512 only) 33 | --help Show this message 34 | -v, --version Show version 35 | 36 | == Using the library 37 | 38 | You can either validate a password of any type matches its hash: 39 | 40 | >> require 'unix_crypt' 41 | => true 42 | >> UnixCrypt.valid?("Hello world!", "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5") 43 | => true 44 | 45 | Or you can generate a new hash, given a password and salt: 46 | 47 | >> UnixCrypt::SHA256.build("Hello world!", "saltstring") 48 | => "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5" 49 | 50 | If a salt is not specified, one will be generated using random data: 51 | 52 | >> UnixCrypt::SHA256.build("Hello world!") 53 | => "$5$v.fjb6lucDCZKjcf$90gzpr9HYo0eAeaN8rubElJdUUOcVYjTnGePBRvCgt1" 54 | 55 | There are four classes you can use, depending on which hashing algorithm you'd like: 56 | 57 | UnixCrypt::DES 58 | UnixCrypt::MD5 59 | UnixCrypt::SHA256 60 | UnixCrypt::SHA512 61 | 62 | == License 63 | 64 | Licensed under the BSD license. See LICENSE file for details. 65 | 66 | == Author 67 | 68 | * Roger Nesbitt (roger@seriousorange.com) 69 | 70 | == Contributors 71 | 72 | * Patrick Wyatt (pat@codeofhonor.com) 73 | -------------------------------------------------------------------------------- /test/unix_crypt/test_command_line.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require File.expand_path('../../../lib/unix_crypt', __FILE__) 3 | require File.expand_path('../../../lib/unix_crypt/command_line', __FILE__) 4 | 5 | class CommandLineTest < Test::Unit::TestCase 6 | class CaptureIO 7 | def initialize(name, buffer, input = []) 8 | @name = name 9 | @buffer = buffer 10 | @input = input 11 | end 12 | 13 | def noecho 14 | yield self 15 | end 16 | 17 | def gets 18 | @buffer << [@name, @input.first.dup] 19 | @input.shift 20 | end 21 | 22 | def write(data) 23 | @buffer << [@name, data] 24 | end 25 | 26 | def print(data) 27 | write data 28 | end 29 | 30 | def puts(data = "") 31 | write "#{data}\n" 32 | end 33 | 34 | def self.redirect(input = []) 35 | buffer = [] 36 | $stdin = new("stdin", buffer, input) 37 | $stdout = new("stdout", buffer) 38 | $stderr = new("stderr", buffer) 39 | yield 40 | buffer 41 | ensure 42 | $stdin = STDIN 43 | $stdout = STDOUT 44 | $stderr = STDERR 45 | end 46 | end 47 | 48 | def test_no_parameter_password_creation 49 | result = CaptureIO.redirect(["hello\n", "hello\n"]) do 50 | UnixCrypt::CommandLine.new([]).encrypt 51 | end 52 | 53 | expected = [ 54 | ["stderr", "Enter password: "], 55 | ["stdin", "hello\n"], 56 | ["stderr", "\n"], 57 | ["stderr", "Verify password: "], 58 | ["stdin", "hello\n"], 59 | ["stderr", "\n"] 60 | ] 61 | 62 | assert_equal expected, result[0..-2] 63 | 64 | channel, password = result[-1] 65 | assert_equal "stdout", channel 66 | assert_match %r{\A\$6\$[a-zA-Z0-9./]{16}\$[a-zA-Z0-9./]{86}\n\z}, password 67 | 68 | assert UnixCrypt.valid?("hello", password) 69 | end 70 | 71 | def test_parameters_provided_password_creation 72 | result = CaptureIO.redirect do 73 | UnixCrypt::CommandLine.new(%w(-h sha256 -p hello -s salty -r 1234)).encrypt 74 | end 75 | 76 | expected = [ 77 | ["stderr", "warning: providing a password on the command line is insecure\n"] 78 | ] 79 | 80 | assert_equal expected, result[0..-2] 81 | 82 | channel, password = result[-1] 83 | assert_equal "stdout", channel 84 | assert_match %r{\A\$5\$rounds=1234\$salty\$[a-zA-Z0-9./]{43}\n\z}, password 85 | 86 | assert UnixCrypt.valid?("hello", password) 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/unix_crypt/sha.rb: -------------------------------------------------------------------------------- 1 | module UnixCrypt 2 | class SHABase < Base 3 | protected 4 | def self.default_salt_length; 16; end 5 | def self.max_salt_length; 16; end 6 | def self.default_rounds; 5000; end 7 | 8 | def self.internal_hash(password, salt, rounds = nil) 9 | rounds = apply_rounds_bounds(rounds || default_rounds) 10 | salt = salt[0..15] 11 | 12 | b = digest.digest("#{password}#{salt}#{password}") 13 | 14 | a_string = password + salt + b * (password.length/length) + b[0...password.length % length] 15 | 16 | password_length = password.length 17 | while password_length > 0 18 | a_string += (password_length & 1 != 0) ? b : password 19 | password_length >>= 1 20 | end 21 | 22 | input = digest.digest(a_string) 23 | 24 | dp = digest.digest(password * password.length) 25 | p = dp * (password.length/length) + dp[0...password.length % length] 26 | 27 | ds = digest.digest(salt * (16 + input.bytes.first)) 28 | s = ds * (salt.length/length) + ds[0...salt.length % length] 29 | 30 | rounds.times do |index| 31 | c_string = ((index & 1 != 0) ? p : input) 32 | c_string += s unless index % 3 == 0 33 | c_string += p unless index % 7 == 0 34 | c_string += ((index & 1 != 0) ? input : p) 35 | input = digest.digest(c_string) 36 | end 37 | 38 | input 39 | end 40 | 41 | def self.apply_rounds_bounds(rounds) 42 | rounds = 1000 if rounds < 1000 43 | rounds = 999_999_999 if rounds > 999_999_999 44 | rounds 45 | end 46 | 47 | def self.rounds_marker(rounds) 48 | if rounds && rounds != default_rounds 49 | "rounds=#{apply_rounds_bounds(rounds)}$" 50 | end 51 | end 52 | end 53 | 54 | class SHA256 < SHABase 55 | protected 56 | def self.digest; Digest::SHA256; end 57 | def self.length; 32; end 58 | def self.identifier; 5; end 59 | 60 | def self.byte_indexes 61 | [[0, 10, 20], [21, 1, 11], [12, 22, 2], [3, 13, 23], [24, 4, 14], [15, 25, 5], [6, 16, 26], [27, 7, 17], [18, 28, 8], [9, 19, 29], [nil, 31, 30]] 62 | end 63 | end 64 | 65 | class SHA512 < SHABase 66 | protected 67 | def self.digest; Digest::SHA512; end 68 | def self.length; 64; end 69 | def self.identifier; 6; end 70 | def self.byte_indexes 71 | [[0, 21, 42], [22, 43, 1], [44, 2, 23], [3, 24, 45], [25, 46, 4], [47, 5, 26], [6, 27, 48], [28, 49, 7], [50, 8, 29], [9, 30, 51], [31, 52, 10], 72 | [53, 11, 32], [12, 33, 54], [34, 55, 13], [56, 14, 35], [15, 36, 57], [37, 58, 16], [59, 17, 38], [18, 39, 60], [40, 61, 19], [62, 20, 41], [nil, nil, 63]] 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/unix_crypt/command_line.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'ostruct' 3 | $no_io_console = false 4 | begin 5 | require 'io/console' 6 | rescue LoadError 7 | $no_io_console = true 8 | end 9 | 10 | class UnixCrypt::CommandLine 11 | Abort = Class.new(StandardError) 12 | 13 | attr_reader :options 14 | 15 | def initialize(argv) 16 | @options = Opts.parse(argv) 17 | end 18 | 19 | def encrypt 20 | if @options.password.nil? 21 | @options.password = ask_password 22 | else 23 | password_warning 24 | end 25 | 26 | begin 27 | puts @options.hasher.build(@options.password, @options.salt, @options.rounds) 28 | rescue UnixCrypt::Error => e 29 | $stderr.puts "password generation failed: #{e.message}" 30 | end 31 | 32 | clear_string(@options.password) 33 | end 34 | 35 | private 36 | class Opts 37 | HASHERS = { 38 | :SHA512 => UnixCrypt::SHA512, 39 | :SHA256 => UnixCrypt::SHA256, 40 | :MD5 => UnixCrypt::MD5, 41 | :DES => UnixCrypt::DES 42 | } 43 | 44 | def self.parse(args) 45 | options = OpenStruct.new 46 | options.hashmethod = :SHA512 47 | options.hasher = HASHERS[options.hashmethod] 48 | options.password = nil 49 | options.salt = nil 50 | options.rounds = nil 51 | options.leftovers = OptionParser.new do |opts| 52 | opts.banner = "Usage: #{File.basename $0} [options]" 53 | opts.separator "Encrypts password using the unix-crypt gem" 54 | opts.separator "" 55 | opts.separator "Options:" 56 | 57 | opts.on("-h", "--hash [HASH]", String, "Set hash algorithm [SHA512 (default), SHA256, MD5, DES]") do |hasher| 58 | options.hashmethod = hasher.to_s.upcase.to_sym 59 | options.hasher = HASHERS[options.hashmethod] 60 | raise Abort, "Invalid hash algorithm for -h/--hash" if options.hasher.nil? 61 | end 62 | 63 | opts.on("-p", "--password [PASSWORD]", String, "Provide password on command line (insecure!)") do |password| 64 | raise Abort, "Invalid password for -p/--password" if password.nil? 65 | options.password = password 66 | $0 = $0 # this invocation will get rid of the command line arguments from the process list 67 | end 68 | 69 | opts.on("-s", "--salt [SALT]", String, "Provide hash salt") do |salt| 70 | raise Abort, "Invalid salt for -s/--salt" if salt.nil? 71 | options.salt = salt 72 | end 73 | 74 | opts.on("-r", "--rounds [ROUNDS]", Integer, "Set number of hashing rounds (SHA256/SHA512 only)") do |rounds| 75 | raise Abort, "Invalid hashing rounds for -r/--rounds" if rounds.nil? || rounds.to_i <= 0 76 | options.rounds = rounds 77 | end 78 | 79 | opts.on_tail("-h", "--help", "Show this message") do 80 | puts opts 81 | exit 82 | end 83 | 84 | opts.on_tail("-v", "--version", "Show version") do 85 | puts UnixCrypt::VERSION 86 | exit 87 | end 88 | end.parse!(args) 89 | options 90 | end 91 | end 92 | 93 | def ask_noecho(message) 94 | $stderr.print message 95 | if $no_io_console 96 | begin 97 | `stty -echo` 98 | result = gets 99 | ensure 100 | `stty echo` 101 | end 102 | else 103 | result = $stdin.noecho(&:gets) 104 | end 105 | $stderr.puts 106 | result 107 | end 108 | 109 | def password_warning 110 | $stderr.puts "warning: providing a password on the command line is insecure" 111 | end 112 | 113 | def clear_string(string) 114 | string.replace(" " * string.length) 115 | end 116 | 117 | def ask_password 118 | password = ask_noecho("Enter password: ") 119 | twice = ask_noecho("Verify password: ") 120 | 121 | if password != twice 122 | clear_string(password) 123 | clear_string(twice) 124 | raise Abort, "Passwords don't match" 125 | end 126 | 127 | clear_string(twice) 128 | password.chomp! 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /test/test_unix_crypt.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # MD5 test cases constructed by Mark Johnston, taken from 4 | # http://code.activestate.com/recipes/325204-passwd-file-compatible-1-md5-crypt/ 5 | # 6 | # SHA test cases found in Ulrich Drepper's paper on SHA crypt, taken from 7 | # http://www.akkadia.org/drepper/SHA-crypt.txt 8 | # 9 | 10 | require 'test/unit' 11 | require File.expand_path('../../lib/unix_crypt', __FILE__) 12 | 13 | class UnixCryptTest < Test::Unit::TestCase 14 | def test_password_validity 15 | tests = [ 16 | # DES 17 | ["PQ", "test", "PQl1.p7BcJRuM"], 18 | ["xx", "much longer password here", "xxtHrOGVa3182"], 19 | 20 | # MD5 21 | [nil, ' ', '$1$yiiZbNIH$YiCsHZjcTkYd31wkgW8JF.'], 22 | [nil, 'pass', '$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90'], 23 | [nil, '____fifteen____', '$1$s9lUWACI$Kk1jtIVVdmT01p0z3b/hw1'], 24 | [nil, '____sixteen_____', '$1$dL3xbVZI$kkgqhCanLdxODGq14g/tW1'], 25 | [nil, '____seventeen____', '$1$NaH5na7J$j7y8Iss0hcRbu3kzoJs5V.'], 26 | [nil, '__________thirty-three___________', '$1$HO7Q6vzJ$yGwp2wbL5D7eOVzOmxpsy.'], 27 | [nil, 'Pässword', '$1$NaH5na7J$MvnEHcxaKZzgBk8QdjdAQ0'], 28 | 29 | # SHA256 30 | ["$5$saltstring", "Hello world!", "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5"], 31 | ["$5$rounds=10000$saltstringsaltstring", "Hello world!", "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA"], 32 | ["$5$rounds=5000$toolongsaltstring", "This is just a test", "$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8mGRcvxa5"], 33 | ["$5$rounds=1400$anotherlongsaltstring", "a very much longer text to encrypt. This one even stretches over morethan one line.", "$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12oP84Bnq1"], 34 | ["$5$rounds=77777$short", "we have a short salt string but not a short password", "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/"], 35 | ["$5$rounds=123456$asaltof16chars..", "a short string", "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/cZKmF/wJvD"], 36 | ["$5$rounds=10$roundstoolow", "the minimum number is still observed", "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL972bIC"], 37 | 38 | # SHA512 39 | ["$6$saltstring", "Hello world!", "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1"], 40 | ["$6$rounds=10000$saltstringsaltstring", "Hello world!", "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v."], 41 | ["$6$rounds=5000$toolongsaltstring", "This is just a test", "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0"], 42 | ["$6$rounds=1400$anotherlongsaltstring", "a very much longer text to encrypt. This one even stretches over morethan one line.", "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1"], 43 | ["$6$rounds=77777$short", "we have a short salt string but not a short password", "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0"], 44 | ["$6$rounds=123456$asaltof16chars..", "a short string", "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"], 45 | ["$6$rounds=10$roundstoolow", "the minimum number is still observed", "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX."] 46 | ] 47 | 48 | tests.each_with_index do |(salt, password, expected), index| 49 | assert UnixCrypt.valid?(password, expected), "Password '#{password}' (index #{index}) failed" 50 | end 51 | end 52 | 53 | def test_validity_of_des_password_generation 54 | hash = UnixCrypt::DES.build("test") 55 | assert UnixCrypt.valid?("test", hash) 56 | 57 | hash = UnixCrypt::DES.build("test", 'xx') 58 | assert UnixCrypt.valid?("test", hash) 59 | end 60 | 61 | def test_validity_of_md5_password_generation 62 | hash = UnixCrypt::MD5.build("test") 63 | assert UnixCrypt.valid?("test", hash) 64 | 65 | hash = UnixCrypt::MD5.build("test", "abcdefgh") 66 | assert UnixCrypt.valid?("test", hash) 67 | end 68 | 69 | def test_validity_of_sha256_password_generation 70 | hash = UnixCrypt::SHA256.build("test") 71 | assert UnixCrypt.valid?("test", hash) 72 | 73 | hash = UnixCrypt::SHA256.build("test", "1234567890123456") 74 | assert UnixCrypt.valid?("test", hash) 75 | end 76 | 77 | def test_validity_of_sha512_password_generation 78 | hash = UnixCrypt::SHA512.build("test") 79 | assert UnixCrypt.valid?("test", hash) 80 | 81 | hash = UnixCrypt::SHA512.build("test", "1234567890123456") 82 | assert UnixCrypt.valid?("test", hash) 83 | end 84 | 85 | def test_structure_of_generated_passwords_and_salts 86 | assert_match %r{\A[a-zA-Z0-9./]{13}\z}, UnixCrypt::DES.build("test password") 87 | assert_match %r{\Azz[a-zA-Z0-9./]{11}\z}, UnixCrypt::DES.build("test password", 'zz') 88 | 89 | assert_match %r{\A\$1\$[a-zA-Z0-9./]{8}\$[a-zA-Z0-9./]{22}\z}, UnixCrypt::MD5.build("test password") 90 | assert_match %r{\A\$1\$abcdefgh\$[a-zA-Z0-9./]{22}\z}, UnixCrypt::MD5.build("test password", "abcdefgh") 91 | 92 | assert_match %r{\A\$5\$[a-zA-Z0-9./]{16}\$[a-zA-Z0-9./]{43}\z}, UnixCrypt::SHA256.build("test password") 93 | assert_match %r{\A\$5\$0123456789abcdef\$[a-zA-Z0-9./]{43}\z}, UnixCrypt::SHA256.build("test password", "0123456789abcdef") 94 | 95 | assert_match %r{\A\$6\$[a-zA-Z0-9./]{16}\$[a-zA-Z0-9./]{86}\z}, UnixCrypt::SHA512.build("test password") 96 | assert_match %r{\A\$6\$0123456789abcdef\$[a-zA-Z0-9./]{86}\z}, UnixCrypt::SHA512.build("test password", "0123456789abcdef") 97 | end 98 | 99 | def test_password_generation_with_rounds 100 | hash = UnixCrypt::SHA512.build("test password", nil, 5678) 101 | assert_match %r{\A\$6\$rounds=5678\$[a-zA-Z0-9./]{16}\$[a-zA-Z0-9./]{86}\z}, hash 102 | assert UnixCrypt.valid?("test password", hash) 103 | 104 | assert_match %r{\A\$6\$rounds=5678\$salted\$[a-zA-Z0-9./]{86}\z}, UnixCrypt::SHA512.build("test password", "salted", 5678) 105 | end 106 | 107 | def test_default_rounds_does_not_add_rounds_marker 108 | assert_match %r{\A\$6\$salted\$[a-zA-Z0-9./]{86}\z}, UnixCrypt::SHA512.build("test password", "salted", 5000) # the default number of rounds 109 | end 110 | 111 | def test_rounds_bounds 112 | hash = UnixCrypt::SHA512.build("test password", nil, 567) 113 | assert_match %r{\A\$6\$rounds=1000\$[a-zA-Z0-9./]{16}\$[a-zA-Z0-9./]{86}\z}, hash 114 | assert UnixCrypt.valid?("test password", hash) 115 | end 116 | 117 | def test_salt_is_not_longer_than_max_length 118 | assert_raise(UnixCrypt::SaltTooLongError) { UnixCrypt::DES.build("test", "123") } 119 | assert_raise(UnixCrypt::SaltTooLongError) { UnixCrypt::MD5.build("test", "123456789") } 120 | assert_raise(UnixCrypt::SaltTooLongError) { UnixCrypt::SHA256.build("test", "12345678901234567") } 121 | end 122 | end 123 | --------------------------------------------------------------------------------