unix-crypt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/unix_crypt.rb +133 -0
  2. data/test/unix_crypt_test.rb +50 -0
  3. metadata +68 -0
data/lib/unix_crypt.rb ADDED
@@ -0,0 +1,133 @@
1
+ require 'digest'
2
+
3
+ module UnixCrypt
4
+ def self.valid?(password, string)
5
+ # Handle the original DES-based crypt(3)
6
+ return password.crypt(string) == string if string.length == 13
7
+
8
+ return false unless m = string.match(/\A\$([156])\$(?:rounds=(\d+)\$)?(.+)\$(.+)/)
9
+ hash = IDENTIFIER_MAPPINGS[m[1]].hash(password, m[3], m[2] && m[2].to_i)
10
+ hash == m[4]
11
+ end
12
+
13
+ class Base
14
+ def self.build(password, salt, rounds = nil)
15
+ "$#{identifier}$#{salt}$#{hash(password, salt)}"
16
+ end
17
+
18
+ protected
19
+ def self.base64encode(input)
20
+ b64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
21
+ output = ""
22
+ byte_indexes.each do |i3, i2, i1|
23
+ b1, b2, b3 = i1 && input[i1] || 0, i2 && input[i2] || 0, i3 && input[i3] || 0
24
+ output <<
25
+ b64[ b1 & 0b00111111] <<
26
+ b64[((b1 & 0b11000000) >> 6) |
27
+ ((b2 & 0b00001111) << 2)] <<
28
+ b64[((b2 & 0b11110000) >> 4) |
29
+ ((b3 & 0b00000011) << 4)] <<
30
+ b64[ (b3 & 0b11111100) >> 2]
31
+ end
32
+
33
+ remainder = 3 - (length % 3)
34
+ remainder = 0 if remainder == 3
35
+ output[0..-1-remainder]
36
+ end
37
+ end
38
+
39
+ class MD5 < Base
40
+ def self.digest; Digest::MD5; end
41
+ def self.length; 16; end
42
+ def self.identifier; 1; end
43
+
44
+ def self.byte_indexes
45
+ [[0, 6, 12], [1, 7, 13], [2, 8, 14], [3, 9, 15], [4, 10, 5], [nil, nil, 11]]
46
+ end
47
+
48
+ def self.hash(password, salt, ignored = nil)
49
+ salt = salt[0..7]
50
+
51
+ b = digest.digest("#{password}#{salt}#{password}")
52
+ a_string = "#{password}$1$#{salt}#{b * (password.length/length)}#{b[0...password.length % length]}"
53
+
54
+ password_length = password.length
55
+ while password_length > 0
56
+ a_string += (password_length & 1 != 0) ? "\x0" : password[0].chr
57
+ password_length >>= 1
58
+ end
59
+
60
+ input = digest.digest(a_string)
61
+
62
+ 1000.times do |index|
63
+ c_string = ((index & 1 != 0) ? password : input)
64
+ c_string += salt unless index % 3 == 0
65
+ c_string += password unless index % 7 == 0
66
+ c_string += ((index & 1 != 0) ? input : password)
67
+ input = digest.digest(c_string)
68
+ end
69
+
70
+ base64encode(input)
71
+ end
72
+ end
73
+
74
+ class SHABase < Base
75
+ def self.hash(password, salt, rounds = nil)
76
+ rounds ||= 5000
77
+ rounds = 1000 if rounds < 1000
78
+ rounds = 999_999_999 if rounds > 999_999_999
79
+
80
+ salt = salt[0..15]
81
+
82
+ b = digest.digest("#{password}#{salt}#{password}")
83
+
84
+ a_string = password + salt + b * (password.length/length) + b[0...password.length % length]
85
+
86
+ password_length = password.length
87
+ while password_length > 0
88
+ a_string += (password_length & 1 != 0) ? b : password
89
+ password_length >>= 1
90
+ end
91
+
92
+ input = a = digest.digest(a_string)
93
+
94
+ dp = digest.digest(password * password.length)
95
+ p = dp * (password.length/length) + dp[0...password.length % length]
96
+
97
+ ds = digest.digest(salt * (16 + a[0]))
98
+ s = ds * (salt.length/length) + ds[0...salt.length % length]
99
+
100
+ rounds.times do |index|
101
+ c_string = ((index & 1 != 0) ? p : input)
102
+ c_string += s unless index % 3 == 0
103
+ c_string += p unless index % 7 == 0
104
+ c_string += ((index & 1 != 0) ? input : p)
105
+ input = digest.digest(c_string)
106
+ end
107
+
108
+ base64encode(input)
109
+ end
110
+ end
111
+
112
+ class SHA256 < SHABase
113
+ def self.digest; Digest::SHA256; end
114
+ def self.length; 32; end
115
+ def self.identifier; 5; end
116
+
117
+ def self.byte_indexes
118
+ [[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]]
119
+ end
120
+ end
121
+
122
+ class SHA512 < SHABase
123
+ def self.digest; Digest::SHA512; end
124
+ def self.length; 64; end
125
+ def self.identifier; 6; end
126
+ def self.byte_indexes
127
+ [[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],
128
+ [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]]
129
+ end
130
+ end
131
+
132
+ IDENTIFIER_MAPPINGS = {'1' => MD5, '5' => SHA256, '6' => SHA512}
133
+ end
@@ -0,0 +1,50 @@
1
+ #
2
+ # MD5 test cases constructed by Mark Johnston, taken from
3
+ # http://code.activestate.com/recipes/325204-passwd-file-compatible-1-md5-crypt/
4
+ #
5
+ # SHA test cases found in Ulrich Drepper's paper on SHA crypt, taken from
6
+ # http://www.akkadia.org/drepper/SHA-crypt.txt
7
+ #
8
+
9
+ require 'test/unit'
10
+ require '../lib/unix_crypt'
11
+
12
+ class UnixCryptTest < Test::Unit::TestCase
13
+ def test_password_validity
14
+ tests = [
15
+ # DES
16
+ ["PQ", "test", "PQl1.p7BcJRuM"],
17
+ ["xx", "much longer password here", "xxtHrOGVa3182"],
18
+
19
+ # MD5
20
+ [nil, ' ', '$1$yiiZbNIH$YiCsHZjcTkYd31wkgW8JF.'],
21
+ [nil, 'pass', '$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90'],
22
+ [nil, '____fifteen____', '$1$s9lUWACI$Kk1jtIVVdmT01p0z3b/hw1'],
23
+ [nil, '____sixteen_____', '$1$dL3xbVZI$kkgqhCanLdxODGq14g/tW1'],
24
+ [nil, '____seventeen____', '$1$NaH5na7J$j7y8Iss0hcRbu3kzoJs5V.'],
25
+ [nil, '__________thirty-three___________', '$1$HO7Q6vzJ$yGwp2wbL5D7eOVzOmxpsy.'],
26
+
27
+ # SHA256
28
+ ["$5$saltstring", "Hello world!", "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5"],
29
+ ["$5$rounds=10000$saltstringsaltstring", "Hello world!", "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA"],
30
+ ["$5$rounds=5000$toolongsaltstring", "This is just a test", "$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8mGRcvxa5"],
31
+ ["$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"],
32
+ ["$5$rounds=77777$short", "we have a short salt string but not a short password", "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/"],
33
+ ["$5$rounds=123456$asaltof16chars..", "a short string", "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/cZKmF/wJvD"],
34
+ ["$5$rounds=10$roundstoolow", "the minimum number is still observed", "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL972bIC"],
35
+
36
+ # SHA512
37
+ ["$6$saltstring", "Hello world!", "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1"],
38
+ ["$6$rounds=10000$saltstringsaltstring", "Hello world!", "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v."],
39
+ ["$6$rounds=5000$toolongsaltstring", "This is just a test", "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0"],
40
+ ["$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"],
41
+ ["$6$rounds=77777$short", "we have a short salt string but not a short password", "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0"],
42
+ ["$6$rounds=123456$asaltof16chars..", "a short string", "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"],
43
+ ["$6$rounds=10$roundstoolow", "the minimum number is still observed", "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX."]
44
+ ]
45
+
46
+ tests.each_with_index do |(salt, password, expected), index|
47
+ assert UnixCrypt.valid?(password, expected), "Password '#{password}' (index #{index}) failed"
48
+ end
49
+ end
50
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unix-crypt
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Roger Nesbitt
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-15 00:00:00 +13:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ 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$)
23
+ email: roger@seriousorange.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/unix_crypt.rb
32
+ - test/unix_crypt_test.rb
33
+ has_rdoc: true
34
+ homepage:
35
+ licenses: []
36
+
37
+ post_install_message:
38
+ rdoc_options: []
39
+
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ hash: 3
48
+ segments:
49
+ - 0
50
+ version: "0"
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.7
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Performs the UNIX crypt(3) algorithm using DES, MD5, SHA256 or SHA512
67
+ test_files: []
68
+