Skip to content

Commit a29a273

Browse files
committed
[compat] implement OpenSSL::KDF.hkdf
1 parent 7be4946 commit a29a273

File tree

3 files changed

+176
-4
lines changed

3 files changed

+176
-4
lines changed

src/main/java/org/jruby/ext/openssl/HMAC.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,9 @@ static void createHMAC(final Ruby runtime, final RubyModule OpenSSL, final RubyC
5555
HMAC.defineAnnotatedMethods(HMAC.class);
5656
}
5757

58-
private static Mac getMacInstance(final String algorithmName) throws NoSuchAlgorithmException {
59-
// final String algorithmSuffix = algorithmName.replaceAll("-", "");
58+
static Mac getMacInstance(final String algorithmName) throws NoSuchAlgorithmException {
6059
final StringBuilder algName = new StringBuilder(5 + algorithmName.length());
61-
algName.append("HMAC"); // .append(algorithmSuffix);
60+
algName.append("HMAC");
6261
for ( int i = 0; i < algorithmName.length(); i++ ) {
6362
char c = algorithmName.charAt(i);
6463
if ( c != '-' ) algName.append(c);
@@ -199,7 +198,7 @@ private byte[] getSignatureBytes() {
199198
return mac.doFinal();
200199
}
201200

202-
private static String getDigestAlgorithmName(final IRubyObject digest) {
201+
static String getDigestAlgorithmName(final IRubyObject digest) {
203202
if ( digest instanceof Digest ) {
204203
return ((Digest) digest).getShortAlgorithm();
205204
}

src/main/java/org/jruby/ext/openssl/KDF.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import java.security.InvalidKeyException;
2727
import java.security.NoSuchAlgorithmException;
28+
import javax.crypto.Mac;
2829

2930
import org.jruby.*;
3031
import org.jruby.anno.JRubyMethod;
@@ -50,6 +51,7 @@ static void createKDF(final Ruby runtime, final RubyModule OpenSSL, final RubyCl
5051
}
5152

5253
private static final String[] PBKDF2_ARGS = new String[] { "salt", "iterations", "length", "hash" };
54+
private static final String[] HKDF_ARGS = new String[] { "salt", "info", "length", "hash" };
5355

5456
@JRubyMethod(module = true) // pbkdf2_hmac(pass, salt:, iterations:, length:, hash:)
5557
public static IRubyObject pbkdf2_hmac(ThreadContext context, IRubyObject self, IRubyObject pass, IRubyObject opts) {
@@ -63,6 +65,61 @@ public static IRubyObject pbkdf2_hmac(ThreadContext context, IRubyObject self, I
6365
}
6466
}
6567

68+
@JRubyMethod(module = true) // hkdf(ikm, salt:, info:, length:, hash:)
69+
public static IRubyObject hkdf(ThreadContext context, IRubyObject self, IRubyObject ikm, IRubyObject opts) {
70+
IRubyObject[] args = extractKeywordArgs(context, (RubyHash) opts, HKDF_ARGS, 0);
71+
try {
72+
return hkdfImpl(context.runtime, ikm, args);
73+
}
74+
catch (NoSuchAlgorithmException|InvalidKeyException e) {
75+
throw newKDFError(context.runtime, e.getMessage());
76+
}
77+
}
78+
79+
static RubyString hkdfImpl(final Ruby runtime, final IRubyObject ikmArg, final IRubyObject[] args)
80+
throws NoSuchAlgorithmException, InvalidKeyException {
81+
final byte[] ikm = ikmArg.convertToString().getBytes();
82+
final byte[] salt = args[0].convertToString().getBytes();
83+
final byte[] info = args[1].convertToString().getBytes();
84+
85+
final long length = RubyNumeric.num2long(args[2]);
86+
if (length < 0) throw runtime.newArgumentError("length must be non-negative");
87+
88+
final Mac mac = getMac(args[3]);
89+
final int macLength = mac.getMacLength();
90+
if (length > 255L * macLength) {
91+
throw newKDFError(runtime, "length must be <= 255 * HashLen");
92+
}
93+
94+
mac.init(new SimpleSecretKey(mac.getAlgorithm(), salt));
95+
final byte[] prk = mac.doFinal(ikm);
96+
97+
mac.init(new SimpleSecretKey(mac.getAlgorithm(), prk));
98+
99+
final byte[] okm = new byte[(int) length];
100+
byte[] block = new byte[0];
101+
int offset = 0;
102+
103+
for (int i = 1; offset < okm.length; i++) {
104+
if (block.length > 0) mac.update(block);
105+
if (info.length > 0) mac.update(info);
106+
mac.update((byte) i);
107+
108+
block = mac.doFinal();
109+
110+
final int copyLength = Math.min(block.length, okm.length - offset);
111+
System.arraycopy(block, 0, okm, offset, copyLength);
112+
offset += copyLength;
113+
}
114+
115+
return StringHelper.newString(runtime, okm);
116+
}
117+
118+
private static Mac getMac(final IRubyObject digest) throws NoSuchAlgorithmException {
119+
final String digestAlg = HMAC.getDigestAlgorithmName(digest);
120+
return HMAC.getMacInstance(digestAlg);
121+
}
122+
66123
static RaiseException newKDFError(Ruby runtime, String message) {
67124
return Utils.newError(runtime, _KDF(runtime).getClass("KDFError"), message);
68125
}

src/test/ruby/test_kdf.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
require File.expand_path('test_helper', File.dirname(__FILE__))
2+
3+
class TestKDF < TestCase
4+
5+
def test_pkcs5_pbkdf2_hmac_compatibility
6+
expected = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 1, length: 20, hash: 'sha1')
7+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac('password', 'salt', 1, 20, 'sha1')
8+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac_sha1('password', 'salt', 1, 20)
9+
end
10+
11+
def test_pbkdf2_hmac_sha1_rfc6070_c_1_len_20
12+
expected = b(%w[0c 60 c8 0f 96 1f 0e 71 f3 a9 b5 24 af 60 12 06 2f e0 37 a6])
13+
value = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 1, length: 20, hash: 'sha1')
14+
assert_equal expected, value
15+
end
16+
17+
def test_pbkdf2_hmac_sha1_rfc6070_c_2_len_20
18+
expected = b(%w[ea 6c 01 4d c7 2d 6f 8c cd 1e d9 2a ce 1d 41 f0 d8 de 89 57])
19+
value = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 2, length: 20, hash: 'sha1')
20+
assert_equal expected, value
21+
end
22+
23+
def test_pbkdf2_hmac_sha1_rfc6070_c_4096_len_20
24+
expected = b(%w[4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1])
25+
value = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 4096, length: 20, hash: 'sha1')
26+
assert_equal expected, value
27+
end
28+
29+
def test_pbkdf2_hmac_sha1_rfc6070_c_4096_len_25
30+
expected = b(%w[3d 2e ec 4f e4 1c 84 9b 80 c8 d8 36 62 c0 e4 4a 8b 29 1a 96 4c f2 f0 70 38])
31+
value = OpenSSL::KDF.pbkdf2_hmac('passwordPASSWORDpassword',
32+
salt: 'saltSALTsaltSALTsaltSALTsaltSALTsalt',
33+
iterations: 4096,
34+
length: 25,
35+
hash: 'sha1')
36+
assert_equal expected, value
37+
end
38+
39+
def test_pbkdf2_hmac_sha1_rfc6070_c_4096_len_16
40+
expected = b(%w[56 fa 6a a7 55 48 09 9d cc 37 d7 f0 34 25 e0 c3])
41+
value = OpenSSL::KDF.pbkdf2_hmac("pass\0word", salt: "sa\0lt", iterations: 4096, length: 16, hash: 'sha1')
42+
assert_equal expected, value
43+
end
44+
45+
def test_pbkdf2_hmac_sha256_c_20000_len_32
46+
password = 'password'
47+
salt = OpenSSL::Random.random_bytes(16)
48+
value1 = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 20_000, length: 32, hash: 'sha256')
49+
value2 = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 20_000, length: 32, hash: 'sha256')
50+
assert_equal value1, value2
51+
end
52+
53+
def test_scrypt_rfc7914_first
54+
skip 'scrypt is not implemented' unless OpenSSL::KDF.respond_to?(:scrypt)
55+
56+
expected = b(%w[
57+
77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97
58+
f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42
59+
fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17
60+
e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06
61+
])
62+
assert_equal expected, OpenSSL::KDF.scrypt('', salt: '', N: 16, r: 1, p: 1, length: 64)
63+
end
64+
65+
def test_scrypt_rfc7914_second
66+
skip 'scrypt is not implemented' unless OpenSSL::KDF.respond_to?(:scrypt)
67+
68+
expected = b(%w[
69+
fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe
70+
7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62
71+
2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da
72+
c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40
73+
])
74+
assert_equal expected, OpenSSL::KDF.scrypt('password', salt: 'NaCl', N: 1024, r: 8, p: 16, length: 64)
75+
end
76+
77+
def test_hkdf_rfc5869_test_case_1
78+
assert_equal b('3cb25f25faacd57a90434f64d0362f2a' \
79+
'2d2d0a90cf1a5a4c5db02d56ecc4c5bf' \
80+
'34007208d5b887185865'),
81+
OpenSSL::KDF.hkdf(b('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'),
82+
salt: b('000102030405060708090a0b0c'),
83+
info: b('f0f1f2f3f4f5f6f7f8f9'),
84+
length: 42,
85+
hash: 'sha256')
86+
end
87+
88+
def test_hkdf_rfc5869_test_case_3
89+
assert_equal b('8da4e775a563c18f715f802a063c5a31' \
90+
'b8a11f5c5ee1879ec3454e5f3c738d2d' \
91+
'9d201395faa4b61a96c8'),
92+
OpenSSL::KDF.hkdf(b('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'),
93+
salt: b(''),
94+
info: b(''),
95+
length: 42,
96+
hash: 'sha256')
97+
end
98+
99+
def test_hkdf_rfc5869_test_case_4
100+
assert_equal b('085a01ea1b10f36933068b56efa5ad81' \
101+
'a4f14b822f5b091568a9cdd4f155fda2' \
102+
'c22e422478d305f3f896'),
103+
OpenSSL::KDF.hkdf(b('0b0b0b0b0b0b0b0b0b0b0b'),
104+
salt: b('000102030405060708090a0b0c'),
105+
info: b('f0f1f2f3f4f5f6f7f8f9'),
106+
length: 42,
107+
hash: 'sha1')
108+
end
109+
110+
private
111+
112+
def b(ary)
113+
[Array(ary).join].pack('H*')
114+
end
115+
116+
end

0 commit comments

Comments
 (0)