unix-crypt 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+