Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jruby/jruby-openssl
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: c340a3bd4613
Choose a base ref
...
head repository: jruby/jruby-openssl
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: c0378b480b89
Choose a head ref

Commits on Jun 1, 2016

  1. Copy the full SHA
    10a1cfc View commit details
  2. Copy the full SHA
    2d8569f View commit details
  3. add an ASN1 encoded EC (private) key reader

    ... including a hack for keeping the curve name oid around
    kares committed Jun 1, 2016
    Copy the full SHA
    f9d390f View commit details
  4. Copy the full SHA
    3748750 View commit details
  5. Copy the full SHA
    c48e3bd View commit details
  6. Copy the full SHA
    5c0aa1b View commit details
  7. Copy the full SHA
    23b6b5d View commit details
  8. Copy the full SHA
    ca28b7e View commit details
  9. Copy the full SHA
    3646eaa View commit details
  10. Copy the full SHA
    dd45cbf View commit details
  11. Copy the full SHA
    f71cbbd View commit details

Commits on Jun 6, 2016

  1. Copy the full SHA
    4d5bead View commit details
  2. handle OpenSSL::Cipher.new('aes-128-gcm') with IV under JRuby

    ... so that it works ~ more compatibly with C OpenSSL (iv_length == 12)
    kares committed Jun 6, 2016
    Copy the full SHA
    7e53018 View commit details
  3. Copy the full SHA
    9173891 View commit details
  4. Copy the full SHA
    90c4332 View commit details
  5. Copy the full SHA
    a8168e3 View commit details

Commits on Jun 7, 2016

  1. Copy the full SHA
    bc30597 View commit details
  2. Copy the full SHA
    8b4566b View commit details
  3. Copy the full SHA
    ff658db View commit details
  4. Copy the full SHA
    6c29e9a View commit details
  5. Copy the full SHA
    c0378b4 View commit details
20 changes: 20 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
## 0.9.17

* support Cipher#auth_tag and auth_data for GCM ciphers (e.g. aes-128-gcm)
* need to drop support for BC <= 1.50 due EC support (N/A in older BCs)
* (somehow working) draft at implementing PKey::EC (elliptic curve support)
DH encryption expected to behave correctly
* make sure (initial) BC security provider registration works!
... when **-Djruby.openssl.provider.register=true** (due #94)
* Make ALL cipherstring match ECDHE cihphers (#91)
* fix X.509 indexBySubject returning correct index
* try to handle `SSLContext.session=` and also try answering `session_reused?`
* handle equals/hashCode on SSL::Session and raise on timeout int overflow
* Allow DSA private keys to be initialized from parameters. (#83)
* Instantiate both the private and public keys when setting parameters. (#82)

## 0.9.16

* add hard dependency to jar-dependencies (#74)
* Recognize Android java.version (#81)

## 0.9.15

* always return a Fixnum from `OpenSSL::SSL::Session#timeout`, OpenSSL defaults
13 changes: 1 addition & 12 deletions Mavenfile
Original file line number Diff line number Diff line change
@@ -100,7 +100,7 @@ plugin :deploy, '2.8.1' do
execute_goals( :deploy, :skip => false )
end

supported_bc_versions = %w{ 1.49 1.50 1.51 1.52 1.53 1.54 }
supported_bc_versions = %w{ 1.51 1.52 1.53 1.54 } # due EC support dropped <= 1.50

default_bc_version = File.read('lib/jopenssl/version.rb')[/BOUNCY_CASTLE_VERSION\s?=\s?'(.*?)'/, 1]

@@ -185,17 +185,6 @@ profile :id => "test-#{version}" do
end
}

#profile :id => 'test-9000' do
# plugin :invoker, '1.8' do
# execute_goals( :install, :run, invoker_run_options )
# end
# # NOTE: we're work-around 9K maven-runit version bug (due minitest changes) !
# # ... still can not build with 9K : https://github.com/jruby/jruby/issues/3184
# properties 'jruby.version' => '9.0.0.0',
# 'jruby.versions' => '9.0.0.0',
# 'bc.versions' => supported_bc_versions.join(',')
#end

profile :id => 'release' do
plugin :gpg, '1.5' do
execute_goal :sign, :phase => :verify
30 changes: 15 additions & 15 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -412,7 +412,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9</jruby.modes>
<jruby.versions>1.6.8</jruby.versions>
</properties>
@@ -450,7 +450,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9</jruby.modes>
<jruby.versions>1.7.4</jruby.versions>
</properties>
@@ -488,7 +488,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.13</jruby.versions>
</properties>
@@ -526,7 +526,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.15</jruby.versions>
</properties>
@@ -564,7 +564,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.16</jruby.versions>
</properties>
@@ -602,7 +602,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.18</jruby.versions>
</properties>
@@ -640,7 +640,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.20</jruby.versions>
</properties>
@@ -678,7 +678,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.22</jruby.versions>
</properties>
@@ -716,7 +716,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.23</jruby.versions>
</properties>
@@ -754,7 +754,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.24</jruby.versions>
</properties>
@@ -792,7 +792,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.modes>1.8,1.9,2.0</jruby.modes>
<jruby.versions>1.7.25</jruby.versions>
</properties>
@@ -830,7 +830,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.version>9.0.1.0</jruby.version>
<jruby.versions>9.0.1.0</jruby.versions>
</properties>
@@ -868,7 +868,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.version>9.0.5.0</jruby.version>
<jruby.versions>9.0.5.0</jruby.versions>
</properties>
@@ -906,7 +906,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.version>9.1.0.0</jruby.version>
<jruby.versions>9.1.0.0</jruby.versions>
</properties>
@@ -944,7 +944,7 @@ DO NOT MODIFIY - GENERATED CODE
</plugins>
</build>
<properties>
<bc.versions>1.49,1.50,1.51,1.52,1.53,1.54</bc.versions>
<bc.versions>1.51,1.52,1.53,1.54</bc.versions>
<jruby.version>9.1.1.0</jruby.version>
<jruby.versions>9.1.1.0</jruby.versions>
</properties>
2 changes: 1 addition & 1 deletion src/main/java/org/jruby/ext/openssl/ASN1.java
Original file line number Diff line number Diff line change
@@ -28,7 +28,6 @@
package org.jruby.ext.openssl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
@@ -111,6 +110,7 @@
import org.jruby.ext.openssl.impl.ASN1Registry;

import static org.jruby.ext.openssl.OpenSSL.*;
import org.jruby.ext.openssl.util.ByteArrayOutputStream;

/**
* @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
205 changes: 172 additions & 33 deletions src/main/java/org/jruby/ext/openssl/Cipher.java
Original file line number Diff line number Diff line change
@@ -38,19 +38,23 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.spec.AlgorithmParameterSpec;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static javax.crypto.Cipher.DECRYPT_MODE;
import static javax.crypto.Cipher.ENCRYPT_MODE;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.RC2ParameterSpec;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyInteger;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
@@ -557,7 +561,12 @@ private static String getPaddingType(final String padding, final String cryptoMo
// TODO check cryptoMode CFB/OFB
final String defaultPadding = "PKCS5Padding";

if ( padding == null ) return defaultPadding;
if ( padding == null ) {
if ( "GCM".equalsIgnoreCase(cryptoMode) ) {
return "NoPadding";
}
return defaultPadding;
}
if ( padding.equalsIgnoreCase("PKCS5Padding") ) {
return "PKCS5Padding";
}
@@ -611,7 +620,9 @@ public int getIvLength() {

if ( ivLength == -1 ) {
if ( "AES".equals(base) ) {
ivLength = 16;
ivLength = 16; // OpenSSL defaults to 12
// NOTE: we can NOT handle 12 for non GCM mode
if ( "GCM".equals(mode) || "CCM".equals(mode) ) ivLength = 12;
}
//else if ( "DES".equals(base) ) {
// ivLength = 8;
@@ -790,22 +801,22 @@ public IRubyObject initialize_copy(final IRubyObject obj) {
}

@JRubyMethod
public IRubyObject name() {
public final RubyString name() {
return getRuntime().newString(name);
}

@JRubyMethod
public IRubyObject key_len() {
public final RubyInteger key_len() {
return getRuntime().newFixnum(keyLength);
}

@JRubyMethod
public IRubyObject iv_len() {
public final RubyInteger iv_len() {
return getRuntime().newFixnum(ivLength);
}

@JRubyMethod(name = "key_len=", required = 1)
public IRubyObject set_key_len(IRubyObject len) {
public final IRubyObject set_key_len(IRubyObject len) {
this.keyLength = RubyNumeric.fix2int(len);
return len;
}
@@ -959,7 +970,7 @@ private void updateCipher(final String name, final String padding) {
cipher = getCipherInstance();
}

javax.crypto.Cipher getCipherInstance() {
final javax.crypto.Cipher getCipherInstance() {
try {
return getCipherInstance(realName, false);
}
@@ -1040,15 +1051,22 @@ else if ( "RC4".equalsIgnoreCase(cryptoBase) ) {
);
}
else {
final AlgorithmParameterSpec ivSpec;
if ( "GCM".equalsIgnoreCase(cryptoMode) ) { // e.g. 'aes-128-gcm'
ivSpec = new GCMParameterSpec(getAuthTagLength() * 8, this.realIV);
}
else {
ivSpec = new IvParameterSpec(this.realIV);
}
cipher.init(encryptMode ? ENCRYPT_MODE : DECRYPT_MODE,
new SimpleSecretKey(getCipherAlgorithm(), this.key),
new IvParameterSpec(this.realIV)
ivSpec
);
}
}
}
catch (InvalidKeyException e) {
throw newCipherError(runtime, e + ": possibly you need to install Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files for your JRE");
throw newCipherError(runtime, e + "\n possibly you need to install Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files for your JRE");
}
catch (Exception e) {
debugStackTrace(runtime, e);
@@ -1071,29 +1089,35 @@ private String getCipherAlgorithm() {
@JRubyMethod
public IRubyObject update(final ThreadContext context, final IRubyObject arg) {
final Ruby runtime = context.runtime;

if ( isDebug(runtime) ) dumpVars( runtime.getOut(), "update()" );

checkCipherNotNull(runtime);
checkAuthTag(runtime);

final byte[] data = arg.asString().getBytes();
if ( data.length == 0 ) {
final ByteList data = arg.asString().getByteList();
final int length = data.length();
if ( length == 0 ) {
throw runtime.newArgumentError("data must not be empty");
}

if ( ! cipherInited ) {
//if ( debug ) runtime.getOut().println("BEFORE INITING");
doInitCipher(runtime);
//if ( debug ) runtime.getOut().println("AFTER INITING");
}
if ( ! cipherInited ) doInitCipher(runtime);

final ByteList str;
try {
final byte[] out = cipher.update(data);
updateAuthData(runtime); // if any

final byte[] in = data.getUnsafeBytes();
final int offset = data.begin();
final byte[] out = cipher.update(in, offset, length);
if ( out != null ) {
str = new ByteList(out, false);
if ( realIV != null ) setLastIVIfNeeded( encryptMode ? out : data );
if ( realIV != null ) {
if ( encryptMode ) setLastIVIfNeeded( out );
else setLastIVIfNeeded( in, offset, length );
}

processedDataBytes += data.length;
processedDataBytes += length;
}
else {
str = new ByteList(ByteList.NULL_ARRAY);
@@ -1115,25 +1139,33 @@ public IRubyObject update_deprecated(final ThreadContext context, final IRubyObj
@JRubyMethod(name = "final")
public IRubyObject do_final(final ThreadContext context) {
final Ruby runtime = context.runtime;

checkCipherNotNull(runtime);
checkAuthTag(runtime);

if ( ! cipherInited ) doInitCipher(runtime);
// trying to allow update after final like cruby-openssl. Bad idea.
if ( "RC4".equalsIgnoreCase(cryptoBase) ) return runtime.newString("");

final ByteList str;
try {
final byte[] out = cipher.doFinal();
if ( out != null ) {
str = new ByteList(out, false);
// TODO: Modifying this line appears to fix the issue, but I do
// not have a good reason for why. Best I can tell, lastIv needs
// to be set regardless of encryptMode, so we'll go with this
// for now. JRUBY-3335.
//if ( realIV != null && encryptMode ) ...
if ( realIV != null ) setLastIVIfNeeded(out);
if ( isAuthDataMode() ) {
str = do_final_with_auth(runtime);
}
else {
str = new ByteList(ByteList.NULL_ARRAY);
final byte[] out = cipher.doFinal();
if ( out != null ) {
// TODO: Modifying this line appears to fix the issue, but I do
// not have a good reason for why. Best I can tell, lastIv needs
// to be set regardless of encryptMode, so we'll go with this
// for now. JRUBY-3335.
//if ( realIV != null && encryptMode ) ...
str = new ByteList(out, false);
if ( realIV != null ) setLastIVIfNeeded(out);
}
else {
str = new ByteList(ByteList.NULL_ARRAY);
}
}

//if ( ! isStreamCipher() ) {
@@ -1159,11 +1191,54 @@ public IRubyObject do_final(final ThreadContext context) {
return RubyString.newString(runtime, str);
}

private ByteList do_final_with_auth(final Ruby runtime) throws GeneralSecurityException {
updateAuthData(runtime); // if any

final ByteList str;
// if GCM/CCM is being used, the authentication tag is appended
// in the case of encryption, or verified in the case of decryption.
// The result is stored in a new buffer.
if ( encryptMode ) {
final byte[] out = cipher.doFinal();

final int len = getAuthTagLength(); int strLen;
if ( ( strLen = out.length - len ) > 0 ) {
str = new ByteList(out, 0, strLen, false);
}
else {
str = new ByteList(ByteList.NULL_ARRAY); strLen = 0;
}
auth_tag = new ByteList(out, strLen, out.length - strLen);
return str;
}
else {
final byte[] out;
if ( auth_tag != null ) {
final byte[] tag = auth_tag.getUnsafeBytes();
out = cipher.doFinal(tag, auth_tag.begin(), auth_tag.length());
}
else {
out = cipher.doFinal();
}
return new ByteList(out, false);
}
}

private void checkAuthTag(final Ruby runtime) {
if ( auth_tag != null && encryptMode ) {
throw newCipherError(runtime, "authentication tag already generated by cipher");
}
}

private void setLastIVIfNeeded(final byte[] tmpIV) {
final int len = ivLength;
if ( lastIV == null ) lastIV = new byte[len];
if ( tmpIV.length >= len ) {
System.arraycopy(tmpIV, tmpIV.length - len, lastIV, 0, len);
setLastIVIfNeeded(tmpIV, 0, tmpIV.length);
}

private void setLastIVIfNeeded(final byte[] tmpIV, final int offset, final int length) {
final int ivLen = this.ivLength;
if ( lastIV == null ) lastIV = new byte[ivLen];
if ( length >= ivLen ) {
System.arraycopy(tmpIV, offset + (length - ivLen), lastIV, 0, ivLen);
}
}

@@ -1173,6 +1248,70 @@ public IRubyObject set_padding(IRubyObject padding) {
return padding;
}

private transient ByteList auth_tag;

@JRubyMethod(name = "auth_tag")
public IRubyObject auth_tag(final ThreadContext context) {
if ( auth_tag != null ) {
return RubyString.newString(context.runtime, auth_tag);
}
if ( ! isAuthDataMode() ) {
throw newCipherError(context.runtime, "authentication tag not supported by this cipher");
}
return context.nil;
}

@JRubyMethod(name = "auth_tag=")
public IRubyObject set_auth_tag(final ThreadContext context, final IRubyObject tag) {
if ( ! isAuthDataMode() ) {
throw newCipherError(context.runtime, "authentication tag not supported by this cipher");
}
final RubyString auth_tag = tag.asString();
this.auth_tag = StringHelper.setByteListShared(auth_tag);
return auth_tag;
}

private boolean isAuthDataMode() { // Authenticated Encryption with Associated Data (AEAD)
return "GCM".equalsIgnoreCase(cryptoMode) || "CCM".equalsIgnoreCase(cryptoMode);
}

private static final int MAX_AUTH_TAG_LENGTH = 16;

private int getAuthTagLength() {
return Math.min(MAX_AUTH_TAG_LENGTH, this.key.length); // in bytes
}

private transient ByteList auth_data;

@JRubyMethod(name = "auth_data=")
public IRubyObject set_auth_data(final ThreadContext context, final IRubyObject data) {
if ( ! isAuthDataMode() ) {
throw newCipherError(context.runtime, "authentication data not supported by this cipher");
}
final RubyString auth_data = data.asString();
this.auth_data = StringHelper.setByteListShared(auth_data);
return auth_data;
}

private boolean updateAuthData(final Ruby runtime) {
if ( auth_data == null ) return false; // only to be set if auth-mode
//try {
final byte[] data = auth_data.getUnsafeBytes();
cipher.updateAAD(data, auth_data.begin(), auth_data.length());
//}
//catch (RuntimeException e) {
// debugStackTrace( runtime, e );
// throw newCipherError(runtime, e);
//}
auth_data = null;
return true;
}

@JRubyMethod(name = "authenticated?")
public RubyBoolean authenticated_p(final ThreadContext context) {
return context.runtime.newBoolean( isAuthDataMode() );
}

@JRubyMethod
public IRubyObject random_key(final ThreadContext context) {
// str = OpenSSL::Random.random_bytes(self.key_len)
2 changes: 1 addition & 1 deletion src/main/java/org/jruby/ext/openssl/PEMUtils.java
Original file line number Diff line number Diff line change
@@ -24,7 +24,6 @@
package org.jruby.ext.openssl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
@@ -60,6 +59,7 @@
import org.jruby.ext.openssl.impl.pem.PEMException;
import org.jruby.ext.openssl.impl.pem.PEMKeyPair;
import org.jruby.ext.openssl.impl.pem.PEMParser;
import org.jruby.ext.openssl.util.ByteArrayOutputStream;
//import org.bouncycastle.util.io.pem.PemReader;

import static org.jruby.ext.openssl.x509store.PEMInputOutput.getKeyFactory;
1 change: 1 addition & 0 deletions src/main/java/org/jruby/ext/openssl/PKey.java
Original file line number Diff line number Diff line change
@@ -85,6 +85,7 @@ public static void createPKey(final Ruby runtime, final RubyModule OpenSSL) {
PKeyRSA.createPKeyRSA(runtime, PKey, PKeyPKey);
PKeyDSA.createPKeyDSA(runtime, PKey, PKeyPKey);
PKeyDH.createPKeyDH(runtime, PKey, PKeyPKey);
PKeyEC.createPKeyEC(runtime, PKey, PKeyPKey);
}

public static RaiseException newPKeyError(Ruby runtime, String message) {
937 changes: 937 additions & 0 deletions src/main/java/org/jruby/ext/openssl/PKeyEC.java

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/main/java/org/jruby/ext/openssl/SecurityHelper.java
Original file line number Diff line number Diff line change
@@ -64,6 +64,8 @@

import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.KeyAgreement;
import javax.crypto.KeyAgreementSpi;
import javax.crypto.KeyGenerator;
import javax.crypto.KeyGeneratorSpi;
import javax.crypto.Mac;
@@ -542,6 +544,30 @@ static KeyGenerator getKeyGenerator(final String algorithm, final Provider provi
);
}

/**
* @note code calling this should not assume BC provider internals !
*/
public static KeyAgreement getKeyAgreement(final String algorithm) throws NoSuchAlgorithmException {
try {
final Provider provider = getSecurityProvider();
if ( provider != null ) return getKeyAgreement(algorithm, provider);
}
catch (NoSuchAlgorithmException e) { }
catch (SecurityException e) { debugStackTrace(e); }
return KeyAgreement.getInstance(algorithm);
}

static KeyAgreement getKeyAgreement(final String algorithm, final Provider provider)
throws NoSuchAlgorithmException {
final KeyAgreementSpi spi = (KeyAgreementSpi) getImplEngine("KeyAgreement", algorithm);
if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " not found");

return newInstance(KeyAgreement.class,
new Class[] { KeyAgreementSpi.class, Provider.class, String.class },
new Object[] { spi, provider, algorithm }
);
}

/**
* @note code calling this should not assume BC provider internals !
*/
16 changes: 16 additions & 0 deletions src/main/java/org/jruby/ext/openssl/StringHelper.java
Original file line number Diff line number Diff line change
@@ -56,6 +56,22 @@ static RubyString newString(final Ruby runtime, final byte[] bytes) {
return RubyString.newString(runtime, byteList);
}

static RubyString newString(final Ruby runtime, final byte[] bytes, final int count) {
final ByteList byteList = new ByteList(bytes, 0, count, false);
return RubyString.newString(runtime, byteList);
}

static ByteList setByteListShared(final RubyString str) {
try {
str.setByteListShared();
return str.getByteList();
}
catch (NoSuchMethodError err) { // JRuby 1.6
RubyString dup = (RubyString) str.dup();
return dup.getByteList();
}
}

static RubyString newUTF8String(final Ruby runtime, final ByteList bytes) {
ByteList byteList = new ByteList(RubyEncoding.encodeUTF8(bytes), UTF8Encoding.INSTANCE, false);
return new RubyString(runtime, runtime.getString(), byteList);
591 changes: 296 additions & 295 deletions src/main/java/org/jruby/ext/openssl/impl/Base64.java

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions src/main/java/org/jruby/ext/openssl/impl/ECPrivateKeyWithName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2016 Karol Bucek.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jruby.ext.openssl.impl;

import java.math.BigInteger;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.ECParameterSpec;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;

/**
* a trick to keep the curve name around
* (since {@link java.security.KeyPair} is final).
*
* @author kares
*/
public final class ECPrivateKeyWithName implements ECPrivateKey {

private final ECPrivateKey realKey;
// private final String curveNameId;
private final ASN1ObjectIdentifier curveNameOID;

public static ECPrivateKeyWithName wrap(ECPrivateKey realKey, ASN1ObjectIdentifier nameOID) {
return new ECPrivateKeyWithName(realKey, nameOID);
}

private ECPrivateKeyWithName(ECPrivateKey realKey, ASN1ObjectIdentifier nameOID) {
this.realKey = realKey; this.curveNameOID = nameOID;
}

//private ECPrivateKeyWithName(ECPrivateKey realKey, String curveNameId) {
// this.realKey = realKey;
// this.curveNameId = curveNameId;
//}

//public String getCurveNameId() {
// return curveNameId;
//}

public ASN1ObjectIdentifier getCurveNameOID() {
return curveNameOID;
}

public ECPrivateKey unwrap() {
return realKey;
}

public BigInteger getS() {
return realKey.getS();
}

public String getAlgorithm() {
return realKey.getAlgorithm();
}

public String getFormat() {
return realKey.getFormat();
}

public byte[] getEncoded() {
return realKey.getEncoded();
}

public ECParameterSpec getParams() {
return realKey.getParams();
}

@Override
public String toString() {
return realKey.toString();
}

}
62 changes: 57 additions & 5 deletions src/main/java/org/jruby/ext/openssl/impl/PKey.java
Original file line number Diff line number Diff line change
@@ -30,7 +30,6 @@
import java.io.IOException;
import java.math.BigInteger;

import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
@@ -39,21 +38,37 @@
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.spec.DHParameterSpec;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.sec.ECPrivateKeyStructure;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;

import org.jruby.ext.openssl.SecurityHelper;

@@ -65,7 +80,8 @@
*/
public class PKey {

public static KeyPair readPrivateKey(byte[] input, String type) throws IOException, GeneralSecurityException {
public static KeyPair readPrivateKey(final byte[] input, final String type)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
KeySpec pubSpec; KeySpec privSpec;
ASN1Sequence seq = (ASN1Sequence) new ASN1InputStream(input).readObject();
if ( type.equals("RSA") ) {
@@ -80,7 +96,8 @@ public static KeyPair readPrivateKey(byte[] input, String type) throws IOExcepti
pubSpec = new RSAPublicKeySpec(mod.getValue(), pubExp.getValue());
privSpec = new RSAPrivateCrtKeySpec(mod.getValue(), pubExp.getValue(), privExp.getValue(), p1.getValue(), p2.getValue(), exp1.getValue(),
exp2.getValue(), crtCoef.getValue());
} else { // assume "DSA" for now.
}
else if ( type.equals("DSA") ) {
ASN1Integer p = (ASN1Integer) seq.getObjectAt(1);
ASN1Integer q = (ASN1Integer) seq.getObjectAt(2);
ASN1Integer g = (ASN1Integer) seq.getObjectAt(3);
@@ -89,6 +106,12 @@ public static KeyPair readPrivateKey(byte[] input, String type) throws IOExcepti
privSpec = new DSAPrivateKeySpec(x.getValue(), p.getValue(), q.getValue(), g.getValue());
pubSpec = new DSAPublicKeySpec(y.getValue(), p.getValue(), q.getValue(), g.getValue());
}
else if ( type.equals("ECDSA") ) {
return readECPrivateKey(input);
}
else {
throw new IllegalStateException("unsupported type: " + type);
}
KeyFactory fact = SecurityHelper.getKeyFactory(type);
return new KeyPair(fact.generatePublic(pubSpec), fact.generatePrivate(privSpec));
}
@@ -147,7 +170,6 @@ public static KeyPair readRSAPrivateKey(final byte[] input)

public static KeyPair readRSAPrivateKey(final KeyFactory rsaFactory, final byte[] input)
throws IOException, InvalidKeySpecException {
// KeyFactory fact = SecurityHelper.getKeyFactory("RSA");
ASN1Sequence seq = (ASN1Sequence) new ASN1InputStream(input).readObject();
if ( seq.size() == 9 ) {
BigInteger mod = ((ASN1Integer) seq.getObjectAt(1)).getValue();
@@ -232,6 +254,36 @@ public static DHParameterSpec readDHParameter(final byte[] input) throws IOExcep
return new DHParameterSpec(p, g);
}

public static KeyPair readECPrivateKey(final byte[] input)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
return readECPrivateKey(SecurityHelper.getKeyFactory("ECDSA"), input);
}

public static KeyPair readECPrivateKey(final KeyFactory ecFactory, final byte[] input)
throws IOException, InvalidKeySpecException {
try {
ECPrivateKeyStructure pKey = new ECPrivateKeyStructure((ASN1Sequence) ASN1Primitive.fromByteArray(input));
AlgorithmIdentifier algId = new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, pKey.getParameters());
PrivateKeyInfo privInfo = new PrivateKeyInfo(algId, pKey.toASN1Primitive());
SubjectPublicKeyInfo pubInfo = new SubjectPublicKeyInfo(algId, pKey.getPublicKey().getBytes());
PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(privInfo.getEncoded());
X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pubInfo.getEncoded());
//KeyFactory fact = KeyFactory.getInstance("ECDSA", provider);

ECPrivateKey privateKey = (ECPrivateKey) ecFactory.generatePrivate(privSpec);
if ( algId.getParameters() instanceof ASN1ObjectIdentifier ) {
privateKey = ECPrivateKeyWithName.wrap(privateKey, (ASN1ObjectIdentifier) algId.getParameters());
}
return new KeyPair(ecFactory.generatePublic(pubSpec), privateKey);
}
catch (ClassCastException ex) {
throw new IOException("wrong ASN.1 object found in stream", ex);
}
//catch (Exception ex) {
// throw new IOException("problem parsing EC private key: " + ex);
//}
}

public static byte[] toDerRSAKey(RSAPublicKey pubKey, RSAPrivateCrtKey privKey) throws IOException {
ASN1EncodableVector vec = new ASN1EncodableVector();
if ( pubKey != null && privKey == null ) {
@@ -268,7 +320,7 @@ public static byte[] toDerDSAKey(DSAPublicKey pubKey, DSAPrivateKey privKey) thr
return new DLSequence(vec).getEncoded();
}
if ( privKey == null ) {
throw new IllegalArgumentException("passed private key as well as public key are null");
throw new IllegalArgumentException("private key as well as public key are null");
}
return privKey.getEncoded();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2016 kares.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jruby.ext.openssl.util;

/**
* Allows direct buffer access for less copy-ing.
*
* @author kares
*/
public final class ByteArrayOutputStream extends java.io.ByteArrayOutputStream {

public ByteArrayOutputStream() {
super();
}

public ByteArrayOutputStream(int size) {
super(size);
}

public byte[] buffer() {
return buf;
}

public int size() {
return count;
}

@Override
public byte[] toByteArray() {
final int len = buf.length;
if (count == len) return buf; // no-copying

final byte[] copy = new byte[count];
System.arraycopy(buf, 0, copy, 0, count);
return copy;
}

}
194 changes: 148 additions & 46 deletions src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/test/ruby/ec/base64.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'base64'

Base64.module_eval do

def self.strict_encode64(bin)
[ bin ].pack("m0")
end unless defined? Base64.strict_encode64

def self.urlsafe_encode64(bin)
strict_encode64(bin).tr("+/", "-_")
end unless defined? Base64.urlsafe_encode64

def self.strict_decode64(str)
str.unpack("m0").first
end unless defined? Base64.strict_decode64

def self.urlsafe_decode64(str)
strict_decode64(str.tr("-_", "+/"))
end unless defined? Base64.urlsafe_decode64

end
136 changes: 136 additions & 0 deletions src/test/ruby/ec/ece.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
class ECE

KEY_LENGTH=16
TAG_LENGTH=16
NONCE_LENGTH=12
SHA256_LENGTH=32

def self.hmac_hash(key, input)
digest = OpenSSL::Digest.new('sha256')
OpenSSL::HMAC.digest(digest, key, input)
end

def self.hkdf_extract(salt, ikm) #ikm stays for input keying material
hmac_hash(salt,ikm)
end

def self.get_info(type, client_public, server_public)
cl_len_no = [client_public.size].pack('n')
sv_len_no = [server_public.size].pack('n')
"Content-Encoding: #{type}\x00P-256\x00#{cl_len_no}#{client_public}#{sv_len_no}#{server_public}"
end

def self.extract_key(params)
raise "Salt must be 16-bytes long" unless params[:salt].length==16

input_key = params[:key]
auth = false
if params.has_key?(:auth) # Encrypted Content Encoding, March 11 2016, http://httpwg.org/http-extensions/draft-ietf-httpbis-encryption-encoding.html
auth = true
input = HKDF.new(input_key, {salt: params[:auth] , algorithm: 'sha256', info: "Content-Encoding: auth\x00"})
input_key = input.next_bytes(SHA256_LENGTH)
secret = HKDF.new(input_key, {salt: params[:salt], algorithm: 'sha256', info: get_info("aesgcm", params[:user_public_key], params[:server_public_key])})
nonce = HKDF.new(input_key, salt: params[:salt], algorithm: 'sha256', info: get_info("nonce", params[:user_public_key], params[:server_public_key]))
else
secret = HKDF.new(input_key, {salt: params[:salt], algorithm: 'sha256', info: "Content-Encoding: aesgcm128"})
nonce = HKDF.new(input_key, salt: params[:salt], algorithm: 'sha256', info: "Content-Encoding: nonce")
end

{key: secret.next_bytes(KEY_LENGTH), nonce: nonce.next_bytes(NONCE_LENGTH), auth: auth}
end

def self.generate_nonce(nonce, counter)
raise "Nonce must be #{NONCE_LENGTH} bytes long." unless nonce.length == NONCE_LENGTH
output = nonce.dup
integer = nonce[-6..-1].unpack('B*')[0].to_i(2) #taking last 6 bytes, treating as integer
x = ((integer ^ counter) & 0xffffff) + ((((integer / 0x1000000) ^ (counter / 0x1000000)) & 0xffffff) * 0x1000000)
bytestring = x.to_s(16).length < 12 ? "0"*(12-x.to_s(16).length)+x.to_s(16) : x.to_s(16) #it's for correct handling of cases when generated integer is less than 6 bytes
output[-6..-1] = [bytestring].pack('H*') #without it packing would produce less than 6 bytes
output #I didn't find pack directive for such usage, so there is a such solution
end

def self.encrypt(data, params)
key = extract_key(params)
rs = params[:rs] ? params [:rs] : 4096
padsize = params[:padsize] ? params [:padsize] : 0
raise "The rs parameter must be greater than 1." if rs <= 1
rs -=1 #this ensures encrypted data cannot be truncated
result = ""
pad_bytes = 1
if params[:auth] # old spec used 1 byte for padding, latest one always uses 2 bytes
pad_bytes = 2
end

counter = 0
(0..data.length).step(rs-pad_bytes+1) do |i|
block = encrypt_record(key, counter, data[i..i+rs-pad_bytes], padsize)
result += block
counter +=1
end
result
end

def self.decrypt(data, params)
key = extract_key(params)
rs = params[:rs] ? params [:rs] : 4096
raise "The rs parameter must be greater than 1." if rs <= 1
rs += TAG_LENGTH
raise "Message is truncated" if data.length % rs == 0
result = ""
counter = 0
(0..data.length).step(rs) do |i|
block = decrypt_record(key, counter, data[i..i+rs-1])
result += block
counter +=1
end
result
end

def self.decrypt_record(params, counter, buffer, pad=0)
gcm = OpenSSL::Cipher.new('aes-128-gcm')
gcm.decrypt
gcm.key = params[:key]
gcm.iv = generate_nonce(params[:nonce], counter)
pad_bytes = 1
if params[:auth] # old spec used 1 byte for padding, latest one always uses 2 bytes
pad_bytes = 2
end
raise "Block is too small" if buffer.length <= TAG_LENGTH+pad_bytes
gcm.auth_tag = buffer[-TAG_LENGTH..-1]
decrypted = gcm.update(buffer[0..-TAG_LENGTH-1]) + gcm.final

if params[:auth]
padding_length = decrypted[0..1].unpack("n")[0]
raise "Padding is too big" if padding_length+2 > decrypted.length
padding = decrypted[2..padding_length]
raise "Wrong padding" unless padding = "\x00"*padding_length
return decrypted[2+padding_length..-1]
else
padding_length = decrypted[0].unpack("C")[0]
raise "Padding is too big" if padding_length+1 > decrypted.length
padding = decrypted[1..padding_length]
raise "Wrong padding" unless padding = "\x00"*padding_length
return decrypted[1..-1]
end
end

def self.encrypt_record(params, counter, buffer, pad=0)
gcm = OpenSSL::Cipher.new('aes-128-gcm')
gcm.encrypt
gcm.key = params[:key]
gcm.iv = generate_nonce(params[:nonce], counter)
gcm.auth_data = ""
padding = ""
if params[:auth]
padding = [pad].pack('n') + "\x00"*pad # 2 bytes, big endian, then n zero bytes of padding
buf = padding+buffer
record = gcm.update(buf)
else
record = gcm.update("\x00"+buffer) # 1 padding byte, not fully implemented
end
enc = record + gcm.final + gcm.auth_tag
enc
end


end
74 changes: 74 additions & 0 deletions src/test/ruby/ec/hkdf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require 'stringio'

class HKDF
DefaultAlgorithm = 'SHA256'
DefaultReadSize = 512 * 1024

def initialize(source, options = {})
source = StringIO.new(source) if source.is_a?(String)

algorithm = options.fetch(:algorithm, DefaultAlgorithm)
@digest = OpenSSL::Digest.new(algorithm)
@info = options.fetch(:info, '')

salt = options[:salt]
salt = 0.chr * @digest.digest_length if salt.nil? or salt.empty?
read_size = options.fetch(:read_size, DefaultReadSize)

@prk = _generate_prk(salt, source, read_size)
@position = 0
@blocks = []
@blocks << ''
end

def algorithm
@digest.name
end

def max_length
@max_length ||= @digest.digest_length * 255
end

def seek(position)
raise RangeError.new("cannot seek past #{max_length}") if position > max_length

@position = position
end

def rewind
seek(0)
end

def next_bytes(length)
new_position = length + @position
raise RangeError.new("requested #{length} bytes, only #{max_length} available") if new_position > max_length

_generate_blocks(new_position)

start = @position
@position = new_position

@blocks.join('').slice(start, length)
end

def next_hex_bytes(length)
next_bytes(length).unpack('H*').first
end

def _generate_prk(salt, source, read_size)
hmac = OpenSSL::HMAC.new(salt, @digest)
while block = source.read(read_size)
hmac.update(block)
end
hmac.digest
end

def _generate_blocks(length)
start = @blocks.size
block_count = (length.to_f / @digest.digest_length).ceil
start.upto(block_count) do |n|
@blocks << OpenSSL::HMAC.digest(@digest, @prk, @blocks[n - 1] + @info + n.chr)
end
end
end

4 changes: 4 additions & 0 deletions src/test/ruby/ec/private_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN EC PRIVATE KEY-----
MD4CAQEEDrtUuJOpUTwJpjf3LJUuoAcGBSuBBAAGoSADHgAEe2LZ/iq6+RafJRYv
bkJPniq3aSf9nv1Xu+DMMg==
-----END EC PRIVATE KEY-----
7 changes: 7 additions & 0 deletions src/test/ruby/ec/private_key2.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN EC PRIVATE KEY-----
MIHaAgEBBEAJF2SrSI8nWVc9JR3qfvmwBpCmb0x5XUc8Tzc1KZ4DtJrg+5Ut6vQR
QK7YIZifynst7q7DODVhgf/D16L8069GoAsGCSskAwMCCAEBDqGBhQOBggAEB/T1
u6sxFny3OW83HXVFXaBUkJtkyByyb3HNuFXSshr3VAozUbHtB8avShcy2jBTULd3
FOzTj5R/ME5egOG1fTMQRSxM85r/cSKFguiJkZGGWETwXvlJ7LRhy5GSeV2fgwLV
TS/ljdy6ho/E+pfViDqIZa+FSTBhbB67TZlbJQw=
-----END EC PRIVATE KEY-----
302 changes: 302 additions & 0 deletions src/test/ruby/ec/test_ec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# coding: US-ASCII
require File.expand_path('../test_helper', File.dirname(__FILE__))

class TestEC < TestCase

def test_read_pem
key_file = File.join(File.dirname(__FILE__), 'private_key.pem')

key = OpenSSL::PKey::EC.new(File.read(key_file))
assert_equal '3799522885287541632525744605009198', key.private_key.to_s

if defined? JRUBY_VERSION
#puts key.to_java.getPublicKey.to_s
#x = key.to_java.getPublicKey.getW.getAffineX
#y = key.to_java.getPublicKey.getW.getAffineY
#puts 'X: ' + x.to_s
#puts 'Y: ' + y.to_s
end

#puts exp = '120833863706476653138797887024101356894168914780792837123086246530098'
#puts key.public_key.to_bn.to_s(16)

assert_equal '120833863706476653138797887024101356894168914780792837123086246530098', key.public_key.to_bn.to_s
assert_equal 'secp112r1', key.group.curve_name
group = key.group
# TODO seems to not match under JRuby+BC ?!
#assert_equal "\x00\xF5\v\x02\x8EMinghuaQu)\x04rx?\xB1", group.seed
assert_equal '1', group.cofactor.to_s
assert_equal '112', group.degree.to_s
assert_equal '4451685225093714776491891542548933', group.order.to_s

#pem = key.to_pem
#puts pem
#assert_equal(pem, OpenSSL::PKey::EC.new(pem).to_pem)
end

def test_read_pem2
key_file = File.join(File.dirname(__FILE__), 'private_key2.pem')

key = OpenSSL::PKey::EC.new(File.read(key_file))
assert_equal '476154198002596104803238069251020502662523042506824360051479804577598604971468345833166876271341274102391714812706759347009731580016190266434110134398790', key.private_key.to_s
assert_equal '724664761298071194184067291718596276558181552214511004334530978676843312147340497441492810597004957502007504373782147802415558443578478808342286909152917608996652942169120263280723117356614533448503051685400082877326382909445989024396491709450773515529281107708539356521507912663674257568110287055756424062220', key.public_key.to_bn.to_s
assert_equal 'brainpoolP512t1', key.group.curve_name
group = key.group
assert_equal nil, group.seed
assert_equal '1', group.cofactor.to_s
assert_equal '512', group.degree.to_s
assert_equal '8948962207650232551656602815159153422162609644098354511344597187200057010413418528378981730643524959857451398370029280583094215613882043973354392115544169', group.order.to_s

#signature = key.dsa_sign_asn1('foo')
#puts signature.inspect
end

def test_point
group = OpenSSL::PKey::EC::Group.new('prime256v1')
client_public_key_bn = OpenSSL::BN.new('58089019511196532477248433747314139754458690644712400444716868601190212265537817278966641566813745621284958192417192818318052462970895792919572995957754854')

binary = "\x04U\x1D6|\xA9\x14\eC\x13\x99b\x96\x9B\x94f\x8F\xB0o\xE2\xD3\xBC%\x8E\xE0Xn\xF2|R\x99b\xBD\xBFB\x8FS\xCF\x13\x7F\x8C\x03N\x96\x9D&\xB2\xE1\xBDQ\b\xCE\x94!s\x06.\xC5?\x96\xC7q\xDA\x8B\xE6"
client_public_key = OpenSSL::PKey::EC::Point.new(group, client_public_key_bn)
assert_equal binary, client_public_key.to_bn.to_s(2)
end

require File.expand_path('base64.rb', File.dirname(__FILE__))

def test_encrypt
p256dh = "BNFege3oh74znsDbVkGf5CRAtLVUHlo5NTU9-inepE_HpUBWUq3FP_dJR-WDORPvKL7fM_AKyfYch-nKY7kDOe0="
group_name = 'prime256v1'

server = OpenSSL::PKey::EC.new(group_name)
# assert server.group.nil? # TODO MRI has a "null" group

server.private_key = OpenSSL::BN.new('107411000028178101972699773683980269641478018566010848092863514011724406285076')
'62303413620263991527470772975506242387142677576794087805856701493545918209262092657150628139966331940997693252502978347289754390001755240229834360187731879'

group = OpenSSL::PKey::EC::Group.new(group_name)
client_public_key_bn = OpenSSL::BN.new(Base64.urlsafe_decode64(p256dh), 2)
# puts client_public_key_bn
client_public_key = OpenSSL::PKey::EC::Point.new(group, client_public_key_bn)

expected = "\xC1\xF9\xF3\xA2-n\xD1z\xBB\xE0\xDA\xDF\xB6\xFF\x8A\xAAR\"\xA7\b\xED\x0E\x83\xAA\x03s\xB0\xECtN\xF4\xC3"
assert_equal expected, server.dh_compute_key(client_public_key)
end

def test_encrypt_integration # inspired by WebPush
require File.expand_path('ece.rb', File.dirname(__FILE__)) unless defined? ECE
require File.expand_path('hkdf.rb', File.dirname(__FILE__)) unless defined? HKDF

p256dh = Base64.urlsafe_encode64 generate_ecdh_key
auth = Base64.urlsafe_encode64 Random.new.bytes(16)

payload = Encryption.encrypt("Hello World", p256dh, auth)

encrypted = payload.fetch(:ciphertext)

decrypted_data = ECE.decrypt(encrypted,
:key => payload.fetch(:shared_secret),
:salt => payload.fetch(:salt),
:server_public_key => payload.fetch(:server_public_key_bn),
:user_public_key => Base64.urlsafe_decode64(p256dh),
:auth => Base64.urlsafe_decode64(auth))

assert_equal "Hello World", decrypted_data
end if RUBY_VERSION > '1.9'

def generate_ecdh_key(group = 'prime256v1')
curve = OpenSSL::PKey::EC.new(group)
curve.generate_key
str = curve.public_key.to_bn.to_s(2)
puts "curve.public_key.to_bn.to_s(2): #{str.inspect}" if $VERBOSE
str
end
private :generate_ecdh_key

module Encryption # EC + (symmetric) AES GCM AAED encryption
extend self

def encrypt(message, p256dh, auth)

group_name = "prime256v1"
salt = Random.new.bytes(16)

server = OpenSSL::PKey::EC.new(group_name)
server.generate_key
server_public_key_bn = server.public_key.to_bn

group = OpenSSL::PKey::EC::Group.new(group_name)
client_public_key_bn = OpenSSL::BN.new(Base64.urlsafe_decode64(p256dh), 2)

#puts client_public_key_bn.to_s if $VERBOSE

client_public_key = OpenSSL::PKey::EC::Point.new(group, client_public_key_bn)

shared_secret = server.dh_compute_key(client_public_key)

client_auth_token = Base64.urlsafe_decode64(auth)

prk = HKDF.new(shared_secret, :salt => client_auth_token, :algorithm => 'SHA256', :info => "Content-Encoding: auth\0").next_bytes(32)

context = create_context(client_public_key_bn, server_public_key_bn)

content_encryption_key_info = create_info('aesgcm', context)
content_encryption_key = HKDF.new(prk, :salt => salt, :info => content_encryption_key_info).next_bytes(16)

nonce_info = create_info('nonce', context)
nonce = HKDF.new(prk, :salt => salt, :info => nonce_info).next_bytes(12)

ciphertext = encrypt_payload(message, content_encryption_key, nonce)

{
:ciphertext => ciphertext, :salt => salt, :shared_secret => shared_secret,
:server_public_key_bn => convert16bit(server_public_key_bn)
}
end

private

def create_context(client_public_key, server_public_key)
c = convert16bit(client_public_key)
s = convert16bit(server_public_key)
context = "\0"
context += [c.bytesize].pack("n*")
context += c
context += [s.bytesize].pack("n*")
context += s
context
end

def encrypt_payload(plaintext, content_encryption_key, nonce)
cipher = OpenSSL::Cipher.new('aes-128-gcm')
cipher.encrypt
cipher.key = content_encryption_key
cipher.iv = nonce
padding = cipher.update("\0\0")
text = cipher.update(plaintext)

e_text = padding + text + cipher.final
e_tag = cipher.auth_tag

e_text + e_tag
end

def create_info(type, context)
info = "Content-Encoding: "
info += type; info += "\0"; info += "P-256"; info += context
info
end

def convert16bit(key)
[key.to_s(16)].pack("H*")
end

end

def setup
super
self.class.disable_security_restrictions!

# @data1 = 'foo'; @data2 = 'bar' * 1000 # data too long for DSA sig

@groups = []; @keys = []

OpenSSL::PKey::EC.builtin_curves.each do |curve, comment|
next if curve.start_with?("Oakley") # Oakley curves are not suitable for ECDSA
group = OpenSSL::PKey::EC::Group.new(curve)

key = OpenSSL::PKey::EC.new(group)
key.generate_key

@groups << group; @keys << key
end
end

def compare_keys(k1, k2)
assert_equal(k1.to_pem, k2.to_pem)
end

def test_builtin_curves
assert(!OpenSSL::PKey::EC.builtin_curves.empty?)
end

def test_curve_names
@groups.each_with_index do |group, idx|
key = @keys[idx]
assert_equal(group.curve_name, key.group.curve_name)
end
end

def test_check_key
for key in @keys
assert_equal(key.check_key, true)
assert_equal(key.private_key?, true)
assert_equal(key.public_key?, true)
end
end

def test_group_encoding
for group in @groups
for meth in [:to_der, :to_pem]
txt = group.send(meth)
gr = OpenSSL::PKey::EC::Group.new(txt)

assert_equal(txt, gr.send(meth))

assert_equal(group.generator.to_bn, gr.generator.to_bn)
assert_equal(group.cofactor, gr.cofactor)
assert_equal(group.order, gr.order)
assert_equal(group.seed, gr.seed)
assert_equal(group.degree, gr.degree)
end
end
end if false # NOT-IMPLEMENTED

def test_key_encoding
for key in @keys
group = key.group

for meth in [:to_der, :to_pem]
txt = key.send(meth)

puts " #{key} #{key.group.curve_name} #{meth.inspect}"

assert_equal(txt, OpenSSL::PKey::EC.new(txt).send(meth))
end

bn = key.public_key.to_bn
assert_equal(bn, OpenSSL::PKey::EC::Point.new(group, bn).to_bn)
end
end if false # NOT-IMPLEMENTED

def test_set_keys
for key in @keys
k = OpenSSL::PKey::EC.new
k.group = key.group
k.private_key = key.private_key
k.public_key = key.public_key

compare_keys(key, k)
end
end if false # NOT-IMPLEMENTED TODO

def test_dsa_sign_verify
data1 = 'foo'
for key in @keys
sig = key.dsa_sign_asn1(data1)
assert(key.dsa_verify_asn1(data1, sig))
end
end if false # NOT-IMPLEMENTED

# def test_dh_compute_key
# for key in @keys
# k = OpenSSL::PKey::EC.new(key.group)
# k.generate_key
#
# puba = key.public_key
# pubb = k.public_key
# a = key.dh_compute_key(pubb)
# b = k.dh_compute_key(puba)
# assert_equal(a, b)
# end
# end

end
125 changes: 125 additions & 0 deletions src/test/ruby/test_cipher.rb
Original file line number Diff line number Diff line change
@@ -68,6 +68,7 @@ def test_instantiate_supported_ciphers
#puts OpenSSL::Cipher.ciphers.size

OpenSSL::Cipher.ciphers.each do |cipher_name|
next if cipher_name.end_with?('wrap') # e.g. 'id-aes256-wrap'
OpenSSL::Cipher.new cipher_name
end
end
@@ -312,6 +313,130 @@ def test_encrypt_aes_cfb_4_incompatibility
end
end

def test_aes_128_gcm
cipher = OpenSSL::Cipher.new('aes-128-gcm')
assert_equal cipher, cipher.encrypt
cipher.key = '01' * 8
cipher.iv = '0' * 16

bytes = '0000' * 4
expected = "\xAC\xC8\x0E\xEDbX,\xB4\xCD\x02\x06O(p\xF8u" # from MRI
actual = cipher.update(bytes)
assert_equal expected, actual
assert_equal "", cipher.final unless defined? JRUBY_VERSION

cipher = OpenSSL::Cipher.new('aes-128-gcm')
assert_equal cipher, cipher.encrypt
cipher.key = '01' * 8
cipher.iv = '012345678' * 2

bytes = '0000' * 4
expected = "\xF3\xEF\xE6K\xBAJ\xAB=7m'\b\xE0\x06U\x9B" # from MRI
actual = cipher.update(bytes)
assert_equal expected, actual
#assert_equal "", cipher.final unless defined? JRUBY_VERSION

cipher = OpenSSL::Cipher.new('aes-128-gcm')
assert_equal cipher, cipher.encrypt
assert_equal 16, cipher.key_len
assert_equal 12, cipher.iv_len
cipher.key = '01' * 8
cipher.iv = '0' * 12

bytes = '0000' * 4
expected = "\xAC\xC8\x0E\xEDbX,\xB4\xCD\x02\x06O(p\xF8u" # from MRI
actual = cipher.update(bytes)
assert_equal expected, actual
#assert_equal "", cipher.final

cipher = OpenSSL::Cipher.new('aes-256-gcm')
assert_equal cipher, cipher.encrypt
assert_equal 32, cipher.key_len
assert_equal 12, cipher.iv_len
cipher.key = '01245678' * 4
cipher.iv = '0123456' * 2

bytes = '0101' * 8
expected = "\xA8I0\xF8\xCD?Z\xFD\x8E\"T\xF5\xF2\xC5\xC8\x05\xD4b\x85\xA3}'\xC99]\xC1\x16\x8B\x13\x9E-)" # from MRI
actual = cipher.update(bytes)
assert_equal expected, actual
#assert_equal "", cipher.final
end

def test_aes_gcm
['aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm'].each do |algo|
pt = "You should all use Authenticated Encryption!"
cipher, key, iv = new_encryptor(algo)

cipher.auth_data = "aad"
ct = cipher.update(pt) + cipher.final
tag = cipher.auth_tag
assert_equal(16, tag.size)

decipher = new_decryptor(algo, key, iv)
decipher.auth_tag = tag
decipher.auth_data = "aad"

assert_equal(pt, decipher.update(ct) + decipher.final)
end
end

def new_encryptor(algo)
cipher = OpenSSL::Cipher.new(algo)
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
[cipher, key, iv]
end
private :new_encryptor

def new_decryptor(algo, key, iv)
OpenSSL::Cipher.new(algo).tap do |cipher|
cipher.decrypt
cipher.key = key
cipher.iv = iv
end
end
private :new_decryptor

def test_aes_128_gcm_with_auth_tag
cipher = OpenSSL::Cipher.new('aes-128-gcm')
cipher.encrypt
#assert_equal 16, cipher.key_len
#assert_equal 12, cipher.iv_len
cipher.key = '01' * 8
cipher.iv = '1001' * 3

plaintext = "Hello World"

padding = cipher.update("\0\0")
text = cipher.update(plaintext)

final = cipher.final; a_tag = cipher.auth_tag

assert_equal "\xB5\xFD", padding unless defined? JRUBY_VERSION
assert_equal "\xCCxqd\xDE\x92\x95\xAD0\xB4=", text unless defined? JRUBY_VERSION
assert_equal "", final unless defined? JRUBY_VERSION

assert_equal "\xB5\xFD\xCCxqd\xDE\x92\x95\xAD0\xB4=", padding + text + final

assert_equal "\ay\xBA\x89\xC9\x91\xF8N\xB7\xD6\x17+\x0F\\\xF8N", a_tag

assert_equal a_tag, cipher.auth_tag
assert_raise(OpenSSL::Cipher::CipherError) { cipher.update("\0\0") }
assert_equal a_tag, cipher.auth_tag
assert_raise(OpenSSL::Cipher::CipherError) { cipher.final }
end

def test_encrypt_auth_data_non_gcm
cipher = OpenSSL::Cipher.new 'aes-128-cfb'
cipher.encrypt
#length = 16
#cipher.iv = '0' * length
#cipher.key = '1' * length
assert_raise(OpenSSL::Cipher::CipherError) { cipher.auth_tag }
end

def test_encrypt_aes_cfb_16_incompatibility
cipher = OpenSSL::Cipher.new 'AES-128-CFB'
assert_equal cipher, cipher.encrypt