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.
- data/lib/unix_crypt.rb +133 -0
- data/test/unix_crypt_test.rb +50 -0
- 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
|
+
|