unix-crypt 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|