Skip to content

Commit dff07ae

Browse files
transclaude
andcommitted
Add Crypt3 module from rubyworks/crypt3
Pure Ruby implementation of crypt(3) — salted one-way password hashing. Replaces Ruby's removed String#crypt with a standalone module. Supports md5, sha1, sha256, sha384, sha512, rmd160. Cleaned up for Ruby 3.x: removed 1.8 compat code, use getbyte instead of [] for byte access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 830bfe4 commit dff07ae

File tree

2 files changed

+128
-1
lines changed

2 files changed

+128
-1
lines changed

HISTORY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ Changes:
99

1010
* New Features
1111

12-
* Add `Array#to_proc` for chaining method calls via array. (PR#233)
12+
* Add `Crypt3` module — pure Ruby crypt(3) implementation (from rubyworks/crypt3).
13+
Supports md5, sha1, sha256, sha384, sha512, rmd160.
1314
* Add `Array#to_ranges` to convert arrays to ranges. (PR#265)
1415
* Add `Array#remove` and `Array#remove!` for count-respecting subtraction. (PR#293)
1516
* Add `Array#indexes` / `Array#index_all` to find all matching indexes. (PR#294)

lib/standard/facets/crypt3.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Crypt3 is a pure Ruby version of crypt(3), a salted one-way hashing
2+
# of a password.
3+
#
4+
# Supported hashing algorithms are: md5, sha1, sha256, sha384, sha512, rmd160.
5+
#
6+
# Only the md5 hashing algorithm is standard and compatible with crypt(3),
7+
# the others are not standard.
8+
#
9+
# Originally written by Poul-Henning Kamp (Beer-Ware License).
10+
# Adapted by guillaume.pierronnet based on FreeBSD src/lib/libcrypt/crypt.c
11+
#
12+
# Copyright (c) 2002 Poul-Henning Kamp
13+
# License: BSD-2-Clause
14+
15+
module Crypt3
16+
17+
VERSION = '1.2.0'
18+
19+
# Base 64 character set.
20+
ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
21+
22+
# A pure ruby version of crypt(3), a salted one-way hashing of a password.
23+
#
24+
# Automatically generates an 8-byte salt if none given.
25+
#
26+
# Crypt3.crypt('password') #=> "$1$xxxxxxxx$hash..."
27+
# Crypt3.crypt('password', :sha256) #=> "$1$xxxxxxxx$hash..."
28+
#
29+
# password - The phrase to encrypt. [String]
30+
# algo - The algorithm to use. [Symbol]
31+
# salt - Cryptographic salt, random if nil. [String]
32+
# magic - The magic prefix. [String]
33+
#
34+
# Returns the cryptographic hash. [String]
35+
def self.crypt(password, algo = :md5, salt = nil, magic = '$1$')
36+
salt ||= generate_salt(8)
37+
38+
case algo
39+
when :md5
40+
require "digest/md5"
41+
when :sha1
42+
require "digest/sha1"
43+
when :rmd160
44+
require "digest/rmd160"
45+
when :sha256, :sha384, :sha512
46+
require "digest/sha2"
47+
else
48+
raise(ArgumentError, "unknown algorithm: #{algo}")
49+
end
50+
51+
digest_class = Digest.const_get(algo.to_s.upcase)
52+
53+
m = digest_class.new
54+
m.update(password + magic + salt)
55+
56+
mixin = digest_class.new.update(password + salt + password).digest
57+
password.length.times do |i|
58+
m.update(mixin[i % 16].chr)
59+
end
60+
61+
i = password.length
62+
while i != 0
63+
if (i & 1) != 0
64+
m.update("\x00")
65+
else
66+
m.update(password[0].chr)
67+
end
68+
i >>= 1
69+
end
70+
71+
final = m.digest
72+
73+
1000.times do |i|
74+
m2 = digest_class.new
75+
m2.update((i & 1) != 0 ? password : final)
76+
m2.update(salt) if (i % 3) != 0
77+
m2.update(password) if (i % 7) != 0
78+
m2.update((i & 1) != 0 ? final : password)
79+
final = m2.digest
80+
end
81+
82+
rearranged = ""
83+
[[0, 6, 12], [1, 7, 13], [2, 8, 14], [3, 9, 15], [4, 10, 5]].each do |a, b, c|
84+
v = final.getbyte(a) << 16 | final.getbyte(b) << 8 | final.getbyte(c)
85+
4.times do
86+
rearranged += ITOA64[v & 0x3f]
87+
v >>= 6
88+
end
89+
end
90+
91+
v = final.getbyte(11)
92+
2.times do
93+
rearranged += ITOA64[v & 0x3f]
94+
v >>= 6
95+
end
96+
97+
magic + salt + '$' + rearranged
98+
end
99+
100+
# Check the validity of a password against a hashed string.
101+
#
102+
# Crypt3.check('password', '$1$xxxxxxxx$hash...') #=> true
103+
#
104+
# password - The phrase that was encrypted. [String]
105+
# hash - The cryptographic hash. [String]
106+
# algo - The algorithm used. [Symbol]
107+
#
108+
# Returns true if valid. [Boolean]
109+
def self.check(password, hash, algo = :md5)
110+
magic, salt = hash.split('$')[1, 2]
111+
magic = '$' + magic + '$'
112+
self.crypt(password, algo, salt, magic) == hash
113+
end
114+
115+
# Generate a random salt of the given size.
116+
#
117+
# Crypt3.generate_salt(8) #=> "xK3d9Wq2"
118+
#
119+
# size - The size of the salt. [Integer]
120+
#
121+
# Returns random salt. [String]
122+
def self.generate_salt(size)
123+
(1..size).map { ITOA64[rand(ITOA64.size)] }.join
124+
end
125+
126+
end

0 commit comments

Comments
 (0)