Skip to content

Commit 0131dc9

Browse files
committed
[feat] support OpenSSL::KDF as a (semi) OpenSSL::PKCS5 replacement
1 parent 991b92a commit 0131dc9

6 files changed

Lines changed: 230 additions & 66 deletions

File tree

lib/openssl/pkcs5.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#--
2+
# Ruby/OpenSSL Project
3+
# Copyright (C) 2017 Ruby/OpenSSL Project Authors
4+
#++
5+
6+
# JOpenSSL has these - here for explicit require 'openssl/pkcs5' compatibility
7+
8+
# module OpenSSL
9+
# module PKCS5
10+
# module_function
11+
#
12+
# # OpenSSL::PKCS5.pbkdf2_hmac has been renamed to OpenSSL::KDF.pbkdf2_hmac.
13+
# # This method is provided for backwards compatibility.
14+
# def pbkdf2_hmac(pass, salt, iter, keylen, digest)
15+
# OpenSSL::KDF.pbkdf2_hmac(pass, salt: salt, iterations: iter, length: keylen, hash: digest)
16+
# end
17+
#
18+
# def pbkdf2_hmac_sha1(pass, salt, iter, keylen)
19+
# pbkdf2_hmac(pass, salt, iter, keylen, "sha1")
20+
# end
21+
# end
22+
# end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2018 Karol Bucek LTD.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package org.jruby.ext.openssl;
25+
26+
import java.security.InvalidKeyException;
27+
import java.security.NoSuchAlgorithmException;
28+
29+
import org.jruby.*;
30+
import org.jruby.anno.JRubyMethod;
31+
import org.jruby.anno.JRubyModule;
32+
import org.jruby.exceptions.RaiseException;
33+
import org.jruby.runtime.ThreadContext;
34+
import org.jruby.runtime.builtin.IRubyObject;
35+
36+
import static org.jruby.ext.openssl.Utils.extractKeywordArgs;
37+
38+
/**
39+
* Provides functionality of various KDFs (key derivation function).
40+
*
41+
* @author kares
42+
*/
43+
@JRubyModule(name = "OpenSSL::KDF")
44+
public class KDF {
45+
46+
static void createKDF(final Ruby runtime, final RubyModule OpenSSL) {
47+
RubyModule KDF = OpenSSL.defineModuleUnder("KDF");
48+
RubyClass OpenSSLError = OpenSSL.getClass("OpenSSLError");
49+
KDF.defineClassUnder("KDFError", OpenSSLError, OpenSSLError.getAllocator());
50+
KDF.defineAnnotatedMethods(KDF.class);
51+
}
52+
53+
private static final String[] PBKDF2_ARGS = new String[] { "salt", "iterations", "length", "hash" };
54+
55+
@JRubyMethod(module = true) // pbkdf2_hmac(pass, salt:, iterations:, length:, hash:)
56+
public static IRubyObject pbkdf2_hmac(ThreadContext context, IRubyObject self, IRubyObject pass, IRubyObject opts) {
57+
IRubyObject[] args = extractKeywordArgs(context, (RubyHash) opts, PBKDF2_ARGS, 1);
58+
args[0] = pass;
59+
try {
60+
return PKCS5.pbkdf2Hmac(context.runtime, args);
61+
}
62+
catch (NoSuchAlgorithmException|InvalidKeyException e) {
63+
throw newKDFError(context.runtime, e.getMessage());
64+
}
65+
}
66+
67+
static RaiseException newKDFError(Ruby runtime, String message) {
68+
return Utils.newError(runtime, _KDF(runtime).getClass("KDFError"), message);
69+
}
70+
71+
static RubyClass _KDF(final Ruby runtime) {
72+
return (RubyClass) runtime.getModule("OpenSSL").getConstant("KDF");
73+
}
74+
75+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public static void createOpenSSL(final Ruby runtime) {
8181
PKCS7.createPKCS7(runtime, _OpenSSL);
8282
PKCS5.createPKCS5(runtime, _OpenSSL);
8383
OCSP.createOCSP(runtime, _OpenSSL);
84+
KDF.createKDF(runtime, _OpenSSL);
8485

8586
runtime.getLoadService().require("jopenssl/version");
8687

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

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@
3434

3535
import org.jruby.Ruby;
3636
import org.jruby.RubyModule;
37+
import org.jruby.RubyNumeric;
3738
import org.jruby.RubyString;
3839
import org.jruby.anno.JRubyMethod;
3940
import org.jruby.anno.JRubyModule;
4041
import org.jruby.runtime.builtin.IRubyObject;
4142

43+
import static org.jruby.ext.openssl.KDF.newKDFError;
44+
4245
/**
4346
* OpenSSL::PKCS5
4447
*
@@ -53,7 +56,7 @@ public static void createPKCS5(final Ruby runtime, final RubyModule ossl) {
5356
}
5457

5558
// def pbkdf2_hmac_sha1(pass, salt, iter, keylen)
56-
@JRubyMethod(meta = true, required = 4)
59+
@JRubyMethod(module = true, required = 4)
5760
public static IRubyObject pbkdf2_hmac_sha1(final IRubyObject self, final IRubyObject[] args) {
5861
//final byte[] pass = args[0].asString().getBytes();
5962
final char[] pass = args[0].asString().toString().toCharArray();
@@ -65,12 +68,26 @@ public static IRubyObject pbkdf2_hmac_sha1(final IRubyObject self, final IRubyOb
6568
}
6669

6770
// def pbkdf2_hmac_sha1(pass, salt, iter, keylen, digest)
68-
@JRubyMethod(meta = true, required = 5)
71+
@JRubyMethod(module = true, required = 5)
6972
public static IRubyObject pbkdf2_hmac(final IRubyObject self, final IRubyObject[] args) {
70-
final byte[] pass = args[0].asString().getBytes();
71-
final byte[] salt = args[1].asString().getBytes();
72-
final int iter = (int) args[2].convertToInteger().getLongValue();
73-
final int keylen = (int) args[3].convertToInteger().getLongValue();
73+
final Ruby runtime = self.getRuntime();
74+
try {
75+
return pbkdf2Hmac(runtime, args);
76+
}
77+
catch (NoSuchAlgorithmException ex) {
78+
throw Utils.newRuntimeError(runtime, ex); // should no happen
79+
}
80+
catch (InvalidKeyException ex) {
81+
throw newKDFError(runtime, ex.getMessage()); // in MRI PKCS5 delegates to KDF impl
82+
}
83+
}
84+
85+
static RubyString pbkdf2Hmac(final Ruby runtime, final IRubyObject[] args)
86+
throws NoSuchAlgorithmException, InvalidKeyException {
87+
final byte[] pass = args[0].convertToString().getBytes();
88+
final byte[] salt = args[1].convertToString().getBytes();
89+
final int iter = RubyNumeric.num2int(args[2]);
90+
final int keylen = RubyNumeric.num2int(args[3]);
7491

7592
final String digestAlg;
7693
final IRubyObject digest = args[4];
@@ -83,20 +100,15 @@ public static IRubyObject pbkdf2_hmac(final IRubyObject self, final IRubyObject[
83100

84101
// NOTE: on our own since e.g. "PBKDF2WithHmacMD5" not supported by Java
85102

103+
// key = SecurityHelper.getSecretKeyFactory("PBKDF2WithHmac" + hash).generateSecret(spec);
104+
// return StringHelper.newString(runtime, key.getEncoded());
105+
86106
final String macAlg = "Hmac" + digestAlg;
87-
final Ruby runtime = self.getRuntime();
88-
try {
89-
final Mac mac = SecurityHelper.getMac( macAlg );
90-
mac.init( new SimpleSecretKey(macAlg, pass) );
91-
final byte[] key = deriveKey(mac, salt, iter, keylen);
92-
return StringHelper.newString(runtime, key);
93-
}
94-
catch (NoSuchAlgorithmException ex) {
95-
throw Utils.newRuntimeError(runtime, ex); // should no happen
96-
}
97-
catch (InvalidKeyException ex) {
98-
throw Utils.newRuntimeError(runtime, ex); // TODO
99-
}
107+
108+
final Mac mac = SecurityHelper.getMac( macAlg );
109+
mac.init( new SimpleSecretKey(macAlg, pass) );
110+
final byte[] key = deriveKey(mac, salt, iter, keylen);
111+
return StringHelper.newString(runtime, key);
100112
}
101113

102114
private static String mapDigestName(final String name) {
@@ -107,18 +119,15 @@ private static String mapDigestName(final String name) {
107119
return mapped;
108120
}
109121

110-
private static RubyString generatePBEKey(final Ruby runtime,
122+
static RubyString generatePBEKey(final Ruby runtime,
111123
final char[] pass, final byte[] salt, final int iter, final int keySize) {
112124
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator();
113125
generator.init(PBEParametersGenerator.PKCS5PasswordToBytes(pass), salt, iter);
114126
CipherParameters params = generator.generateDerivedParameters(keySize * 8);
115127
return StringHelper.newString(runtime, ((KeyParameter) params).getKey());
116128
}
117129

118-
// http://stackoverflow.com/questions/9147463/java-pbkdf2-with-hmacsha256-as-the-prf
119-
120-
public static byte[] deriveKey( final Mac prf, byte[] salt, int iterationCount, int dkLen )
121-
throws NoSuchAlgorithmException, InvalidKeyException {
130+
public static byte[] deriveKey( final Mac prf, byte[] salt, int iterationCount, int dkLen ) {
122131

123132
// Note: hLen, dkLen, l, r, T, F, etc. are horrible names for
124133
// variables and functions in this day and age, but they

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@
2828
package org.jruby.ext.openssl;
2929

3030
import java.io.IOException;
31+
import java.util.HashSet;
3132

32-
import org.jruby.Ruby;
33-
import org.jruby.RubyClass;
34-
import org.jruby.RubyModule;
33+
import org.jruby.*;
3534
import org.jruby.exceptions.RaiseException;
3635
import org.jruby.internal.runtime.methods.DynamicMethod;
3736
import org.jruby.internal.runtime.methods.UndefinedMethod;
3837
import org.jruby.runtime.Block;
3938
import org.jruby.runtime.ThreadContext;
4039
import org.jruby.runtime.builtin.IRubyObject;
40+
import org.jruby.util.TypeConverter;
4141

4242
/**
4343
* @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
@@ -149,4 +149,40 @@ private static <T extends Throwable> void throwsUnchecked(Throwable t) throws T
149149
throw (T) t;
150150
}
151151

152+
// from JRuby's ArgsUtil :
153+
154+
static IRubyObject[] extractKeywordArgs(final ThreadContext context, RubyHash options, String... validKeys) {
155+
return extractKeywordArgs(context, options, validKeys, 0);
156+
}
157+
158+
static IRubyObject[] extractKeywordArgs(final ThreadContext context, RubyHash options, String[] validKeys, int offset) {
159+
final IRubyObject[] ret = new IRubyObject[offset + validKeys.length];
160+
161+
final HashSet<RubySymbol> validKeySet = new HashSet<>(ret.length);
162+
163+
// Build the return values
164+
for (int i=0; i<validKeys.length; i++) {
165+
final String key = validKeys[i];
166+
RubySymbol keySym = context.runtime.newSymbol(key);
167+
IRubyObject val = options.fastARef(keySym);
168+
ret[offset + i] = val != null ? val : RubyBasicObject.UNDEF;
169+
validKeySet.add(keySym);
170+
}
171+
172+
// Check for any unknown keys
173+
options.visitAll(new RubyHash.Visitor() {
174+
public void visit(IRubyObject key, IRubyObject value) {
175+
if (!validKeySet.contains(key)) {
176+
throw context.runtime.newArgumentError("unknown keyword: " + key);
177+
}
178+
}
179+
});
180+
181+
return ret;
182+
}
183+
184+
static IRubyObject extractKeywordArg(ThreadContext context, String keyword, RubyHash opts) {
185+
return opts.op_aref(context, context.runtime.newSymbol(keyword));
186+
}
187+
152188
}// Utils

src/test/ruby/pkcs5/test_pbkdf2.rb

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,65 @@
11
require File.expand_path('../test_helper', File.dirname(__FILE__))
22

3-
module Jopenssl
4-
class TestPKCS5 < TestCase
5-
6-
def test_pbkdf2_hmac_sha1
7-
pass = 'secret'
8-
salt = 'sugar0'
9-
iter = 42
10-
keylen = 24
11-
expected = "\a\xB6I\xE1)\xD8\xA6\x84\xC8D\b\xB2h(]\xBA\x87\xDE\e\xFC\x7F\e\xC3\x06"
12-
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
13-
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, iter, keylen)
14-
end
15-
16-
def test_pbkdf2_hmac_sha1_with_empty_salt
17-
pass = ' '
18-
expected = "\x81\e\xE9F\xD8op\xA6\x9D\xF4=\tX\x13\x82D\xF7\xF3\x7F\xC8aFR+"
19-
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
20-
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, '', 16, 24)
21-
end
22-
23-
def test_pbkdf2_hmac
24-
pass = 'SecreT2'
25-
salt = '0123456789001234567890'
26-
27-
digest = OpenSSL::Digest::MD5.new
28-
expected = "\xC10D2\x8F\xEA}\xF7ag\xB5\xC8Ad\xFBN9Ff\x9D}\xA6\a\x86\x8F\xC4&HI\x85\x89<cGl\x02W\xF9\xD8\xF9\x1C\xAB\xFF\xA3\xC9C>U"
29-
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
30-
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 120, 48, digest)
31-
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 120, 48, digest)
32-
33-
digest = OpenSSL::Digest::SHA256.new
34-
expected = "}\xF4\xE3\xBF\xA7u\xB3[l\xE0(\x84\x96W\xFA\x00h\xA1l#\xB8\xC0Ptirz\v\xBA\x0Es\n<\xF8\xB5(\x85\xDA\xFE\x02y\x14\xB5A`\x8F\xA3\x03\x95\xA7G\xB4pU\xB6pf=Q\x1Fz\x12u\x83"
35-
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
36-
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 100, 64, digest)
37-
38-
expected = "\x03\x1C\x86\xC7N?\xC3\xBC\xF30W\xEC\x9B\x89I\x8D\xE6|\xA1Y\xEF\bt\xB4\x17\xA9\x87\xCB\xEA\x7F\x92\xDB\x88N@\xCB\x17\xDF\xC4\x8F\xE48L\x1Dy<\xD8\x9B\x8Cx\x85\x93\n\xA3`\xE9]\x90\xA2\x10I[\xE9\x84"
39-
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
40-
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 100, 64, 'SHA512')
41-
end
3+
class TestPKCS5 < TestCase
424

5+
def test_pbkdf2_hmac_sha1
6+
pass = 'secret'
7+
salt = 'sugar0'
8+
iter = 42
9+
keylen = 24
10+
expected = "\a\xB6I\xE1)\xD8\xA6\x84\xC8D\b\xB2h(]\xBA\x87\xDE\e\xFC\x7F\e\xC3\x06"
11+
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
12+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, iter, keylen)
4313
end
14+
15+
def test_pbkdf2_hmac_sha1_with_empty_salt
16+
pass = ' '
17+
expected = "\x81\e\xE9F\xD8op\xA6\x9D\xF4=\tX\x13\x82D\xF7\xF3\x7F\xC8aFR+"
18+
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
19+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, '', 16, 24)
20+
end
21+
22+
def test_pbkdf2_hmac
23+
pass = 'SecreT2'
24+
salt = '0123456789001234567890'
25+
26+
digest = OpenSSL::Digest::MD5.new
27+
expected = "\xC10D2\x8F\xEA}\xF7ag\xB5\xC8Ad\xFBN9Ff\x9D}\xA6\a\x86\x8F\xC4&HI\x85\x89<cGl\x02W\xF9\xD8\xF9\x1C\xAB\xFF\xA3\xC9C>U"
28+
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
29+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 120, 48, digest)
30+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 120, 48, digest)
31+
32+
digest = OpenSSL::Digest::SHA256.new
33+
expected = "}\xF4\xE3\xBF\xA7u\xB3[l\xE0(\x84\x96W\xFA\x00h\xA1l#\xB8\xC0Ptirz\v\xBA\x0Es\n<\xF8\xB5(\x85\xDA\xFE\x02y\x14\xB5A`\x8F\xA3\x03\x95\xA7G\xB4pU\xB6pf=Q\x1Fz\x12u\x83"
34+
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
35+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 100, 64, digest)
36+
37+
expected = "\x03\x1C\x86\xC7N?\xC3\xBC\xF30W\xEC\x9B\x89I\x8D\xE6|\xA1Y\xEF\bt\xB4\x17\xA9\x87\xCB\xEA\x7F\x92\xDB\x88N@\xCB\x17\xDF\xC4\x8F\xE48L\x1Dy<\xD8\x9B\x8Cx\x85\x93\n\xA3`\xE9]\x90\xA2\x10I[\xE9\x84"
38+
expected.force_encoding('ASCII-8BIT') if ''.respond_to?(:force_encoding)
39+
assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac(pass, salt, 100, 64, 'SHA512')
40+
end
41+
42+
43+
def test_pbkdf2_hmac_sha1_rfc6070_c_4096_len_16
44+
p ="pass\0word"
45+
s = "sa\0lt"
46+
c = 4096
47+
len = 16
48+
raw = %w{ 56 fa 6a a7 55 48 09 9d cc 37 d7 f0 34 25 e0 c3 }
49+
expected = [raw.join('')].pack('H*')
50+
value = OpenSSL::KDF.pbkdf2_hmac(p, salt: s, iterations: c, length: len, hash: 'sha1')
51+
assert_equal(expected, value)
52+
end
53+
54+
def test_pbkdf2_hmac_sha256_c_20000_len_32
55+
p ="password"
56+
s = OpenSSL::Random.random_bytes(16)
57+
c = 20000
58+
len = 32
59+
digest = OpenSSL::Digest::SHA256.new
60+
value1 = OpenSSL::PKCS5.pbkdf2_hmac(p, s, c, len, digest)
61+
value2 = OpenSSL::KDF.pbkdf2_hmac(p, salt: s, iterations: c, length: len, hash: digest)
62+
assert_equal(value1, value2)
63+
end
64+
4465
end

0 commit comments

Comments
 (0)