Skip to content

Commit a8168e3

Browse files
committed
implement Cipher#auth_data= and support decryption with GCM mode
1 parent 90c4332 commit a8168e3

File tree

2 files changed

+133
-45
lines changed

2 files changed

+133
-45
lines changed

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

Lines changed: 97 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,26 +1090,23 @@ private String getCipherAlgorithm() {
10901090
public IRubyObject update(final ThreadContext context, final IRubyObject arg) {
10911091
final Ruby runtime = context.runtime;
10921092

1093-
if ( auth_tag != null ) throw newAuthTagPresentError(runtime);
1094-
10951093
if ( isDebug(runtime) ) dumpVars( runtime.getOut(), "update()" );
10961094

10971095
checkCipherNotNull(runtime);
1096+
checkAuthTag(runtime);
10981097

10991098
final ByteList data = arg.asString().getByteList();
11001099
final int length = data.length();
11011100
if ( length == 0 ) {
11021101
throw runtime.newArgumentError("data must not be empty");
11031102
}
11041103

1105-
if ( ! cipherInited ) {
1106-
//if ( debug ) runtime.getOut().println("BEFORE INITING");
1107-
doInitCipher(runtime);
1108-
//if ( debug ) runtime.getOut().println("AFTER INITING");
1109-
}
1104+
if ( ! cipherInited ) doInitCipher(runtime);
11101105

11111106
final ByteList str;
11121107
try {
1108+
updateAuthData(runtime); // if any
1109+
11131110
final byte[] in = data.getUnsafeBytes();
11141111
final int offset = data.begin();
11151112
final byte[] out = cipher.update(in, offset, length);
@@ -1143,43 +1140,32 @@ public IRubyObject update_deprecated(final ThreadContext context, final IRubyObj
11431140
public IRubyObject do_final(final ThreadContext context) {
11441141
final Ruby runtime = context.runtime;
11451142

1146-
if ( auth_tag != null ) throw newAuthTagPresentError(runtime);
1147-
11481143
checkCipherNotNull(runtime);
1144+
checkAuthTag(runtime);
1145+
11491146
if ( ! cipherInited ) doInitCipher(runtime);
11501147
// trying to allow update after final like cruby-openssl. Bad idea.
11511148
if ( "RC4".equalsIgnoreCase(cryptoBase) ) return runtime.newString("");
11521149

1153-
final ByteList str; boolean shared = false;
1150+
final ByteList str;
11541151
try {
1155-
final byte[] out = cipher.doFinal();
1156-
if ( out != null ) {
1157-
if ( isAuthDataMode() ) { // TODO only implemented encryption !
1158-
1159-
// if GCM/CCM is being used, the authentication tag is appended
1160-
// in the case of encryption, or verified in the case of decryption.
1161-
// The result is stored in a new buffer.
1162-
final int len = getAuthTagLength(); int strLen;
1163-
if ( ( strLen = out.length - len ) > 0 ) {
1164-
str = new ByteList(out, 0, strLen, false); shared = true;
1165-
}
1166-
else {
1167-
str = new ByteList(ByteList.NULL_ARRAY); strLen = 0;
1168-
}
1169-
auth_tag = new ByteList(out, strLen, out.length - strLen);
1170-
}
1171-
// TODO: Modifying this line appears to fix the issue, but I do
1172-
// not have a good reason for why. Best I can tell, lastIv needs
1173-
// to be set regardless of encryptMode, so we'll go with this
1174-
// for now. JRUBY-3335.
1175-
//if ( realIV != null && encryptMode ) ...
1176-
else {
1152+
if ( isAuthDataMode() ) {
1153+
str = do_final_with_auth(runtime);
1154+
}
1155+
else {
1156+
final byte[] out = cipher.doFinal();
1157+
if ( out != null ) {
1158+
// TODO: Modifying this line appears to fix the issue, but I do
1159+
// not have a good reason for why. Best I can tell, lastIv needs
1160+
// to be set regardless of encryptMode, so we'll go with this
1161+
// for now. JRUBY-3335.
1162+
//if ( realIV != null && encryptMode ) ...
11771163
str = new ByteList(out, false);
11781164
if ( realIV != null ) setLastIVIfNeeded(out);
11791165
}
1180-
}
1181-
else {
1182-
str = new ByteList(ByteList.NULL_ARRAY);
1166+
else {
1167+
str = new ByteList(ByteList.NULL_ARRAY);
1168+
}
11831169
}
11841170

11851171
//if ( ! isStreamCipher() ) {
@@ -1202,18 +1188,46 @@ public IRubyObject do_final(final ThreadContext context) {
12021188
debugStackTrace(runtime, e);
12031189
throw newCipherError(runtime, e);
12041190
}
1205-
return shared ? RubyString.newStringShared(runtime, str) : RubyString.newString(runtime, str);
1191+
return RubyString.newString(runtime, str);
12061192
}
12071193

1208-
private RaiseException newAuthTagPresentError(final Ruby runtime) {
1209-
final String error;
1210-
//if ( encryptMode ) {
1211-
error = "authentication tag already generated by cipher";
1212-
//}
1213-
//else {
1214-
//error = "authentication tag already consumed by cipher";
1215-
//}
1216-
return newCipherError(runtime, error);
1194+
private ByteList do_final_with_auth(final Ruby runtime) throws GeneralSecurityException {
1195+
updateAuthData(runtime); // if any
1196+
1197+
final ByteList str;
1198+
// if GCM/CCM is being used, the authentication tag is appended
1199+
// in the case of encryption, or verified in the case of decryption.
1200+
// The result is stored in a new buffer.
1201+
if ( encryptMode ) {
1202+
final byte[] out = cipher.doFinal();
1203+
1204+
final int len = getAuthTagLength(); int strLen;
1205+
if ( ( strLen = out.length - len ) > 0 ) {
1206+
str = new ByteList(out, 0, strLen, false);
1207+
}
1208+
else {
1209+
str = new ByteList(ByteList.NULL_ARRAY); strLen = 0;
1210+
}
1211+
auth_tag = new ByteList(out, strLen, out.length - strLen);
1212+
return str;
1213+
}
1214+
else {
1215+
final byte[] out;
1216+
if ( auth_tag != null ) {
1217+
final byte[] tag = auth_tag.getUnsafeBytes();
1218+
out = cipher.doFinal(tag, auth_tag.begin(), auth_tag.length());
1219+
}
1220+
else {
1221+
out = cipher.doFinal();
1222+
}
1223+
return new ByteList(out, false);
1224+
}
1225+
}
1226+
1227+
private void checkAuthTag(final Ruby runtime) {
1228+
if ( auth_tag != null && encryptMode ) {
1229+
throw newCipherError(runtime, "authentication tag already generated by cipher");
1230+
}
12171231
}
12181232

12191233
private void setLastIVIfNeeded(final byte[] tmpIV) {
@@ -1247,6 +1261,17 @@ public IRubyObject auth_tag(final ThreadContext context) {
12471261
return context.nil;
12481262
}
12491263

1264+
@JRubyMethod(name = "auth_tag=")
1265+
public IRubyObject set_auth_tag(final ThreadContext context, final IRubyObject tag) {
1266+
if ( ! isAuthDataMode() ) {
1267+
throw newCipherError(context.runtime, "authentication tag not supported by this cipher");
1268+
}
1269+
final RubyString auth_tag = tag.asString();
1270+
this.auth_tag = auth_tag.getByteList();
1271+
auth_tag.setByteListShared();
1272+
return auth_tag;
1273+
}
1274+
12501275
private boolean isAuthDataMode() { // Authenticated Encryption with Associated Data (AEAD)
12511276
return "GCM".equalsIgnoreCase(cryptoMode) || "CCM".equalsIgnoreCase(cryptoMode);
12521277
}
@@ -1257,6 +1282,33 @@ private int getAuthTagLength() {
12571282
return Math.min(MAX_AUTH_TAG_LENGTH, this.key.length); // in bytes
12581283
}
12591284

1285+
private transient ByteList auth_data;
1286+
1287+
@JRubyMethod(name = "auth_data=")
1288+
public IRubyObject set_auth_data(final ThreadContext context, final IRubyObject data) {
1289+
if ( ! isAuthDataMode() ) {
1290+
throw newCipherError(context.runtime, "authentication data not supported by this cipher");
1291+
}
1292+
final RubyString auth_data = data.asString();
1293+
this.auth_data = auth_data.getByteList();
1294+
auth_data.setByteListShared();
1295+
return auth_data;
1296+
}
1297+
1298+
private boolean updateAuthData(final Ruby runtime) {
1299+
if ( auth_data == null ) return false; // only to be set if auth-mode
1300+
//try {
1301+
final byte[] data = auth_data.getUnsafeBytes();
1302+
cipher.updateAAD(data, auth_data.begin(), auth_data.length());
1303+
//}
1304+
//catch (RuntimeException e) {
1305+
// debugStackTrace( runtime, e );
1306+
// throw newCipherError(runtime, e);
1307+
//}
1308+
auth_data = null;
1309+
return true;
1310+
}
1311+
12601312
@JRubyMethod(name = "authenticated?")
12611313
public RubyBoolean authenticated_p(final ThreadContext context) {
12621314
return context.runtime.newBoolean( isAuthDataMode() );

src/test/ruby/test_cipher.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,42 @@ def test_aes_128_gcm
363363
#assert_equal "", cipher.final
364364
end
365365

366+
def test_aes_gcm
367+
['aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm'].each do |algo|
368+
pt = "You should all use Authenticated Encryption!"
369+
cipher, key, iv = new_encryptor(algo)
370+
371+
cipher.auth_data = "aad"
372+
ct = cipher.update(pt) + cipher.final
373+
tag = cipher.auth_tag
374+
assert_equal(16, tag.size)
375+
376+
decipher = new_decryptor(algo, key, iv)
377+
decipher.auth_tag = tag
378+
decipher.auth_data = "aad"
379+
380+
assert_equal(pt, decipher.update(ct) + decipher.final)
381+
end
382+
end
383+
384+
def new_encryptor(algo)
385+
cipher = OpenSSL::Cipher.new(algo)
386+
cipher.encrypt
387+
key = cipher.random_key
388+
iv = cipher.random_iv
389+
[cipher, key, iv]
390+
end
391+
private :new_encryptor
392+
393+
def new_decryptor(algo, key, iv)
394+
OpenSSL::Cipher.new(algo).tap do |cipher|
395+
cipher.decrypt
396+
cipher.key = key
397+
cipher.iv = iv
398+
end
399+
end
400+
private :new_decryptor
401+
366402
def test_aes_128_gcm_with_auth_tag
367403
cipher = OpenSSL::Cipher.new('aes-128-gcm')
368404
cipher.encrypt

0 commit comments

Comments
 (0)