Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.security.Provider;
import java.security.Security;
import java.util.Arrays;
import java.util.HashMap;

public class CipherUtils {
Expand Down Expand Up @@ -41,9 +42,30 @@ private void getCipherInfos() {
}
}
}
/*
* Since JDK 9 the providers no longer list GCM in "SupportedModes";
* it is only registered as complete transformations such as
* "Cipher.AES/GCM/NoPadding". Lift GCM out of those keys in a second
* pass so the unordered property iteration above cannot overwrite it.
*/
for (Provider provider : Security.getProviders()) {
for (String key : provider.stringPropertyNames()) {
if (!key.startsWith("Cipher.") || key.contains(" ")) {
continue;
}
String[] transformation = key.substring(7).split("/");
if (transformation.length != 3 || !transformation[1].equalsIgnoreCase("GCM")) {
continue;
}
CipherInfo info = algos.getOrDefault(transformation[0], new CipherInfo());
info.addMode("GCM");
this.algos.put(transformation[0], info);
}
}

// Add info for SM4
CipherInfo info = new CipherInfo();
info.setModes(new String[]{"ECB", "CBC", "CTR", "OFB", "CFB"});
info.setModes(new String[]{"ECB", "CBC", "CTR", "OFB", "CFB", "GCM"});
info.setPaddings(new String[]{"NOPADDING", "PKCS5PADDING"});
algos.put("SM4", info);
}
Expand Down Expand Up @@ -83,6 +105,16 @@ public void setModes(String[] modes) {
this.modes = modes;
}

public void addMode(String mode) {
for (String existing : this.modes) {
if (existing.equalsIgnoreCase(mode)) {
return;
}
}
this.modes = Arrays.copyOf(this.modes, this.modes.length + 1);
this.modes[this.modes.length - 1] = mode;
}

public String[] getPaddings() {
return paddings;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.security.Security;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.swing.JComboBox;
Expand All @@ -26,6 +27,8 @@ public abstract class CryptOperation extends Operation {

private static String[] inOutModes = new String[] { "Raw", "Hex", "Base64" };

private static final int GCM_TAG_LENGTH_BITS = 128;

protected String algorithm;
protected FormatTextField ivTxt;
protected FormatTextField keyTxt;
Expand All @@ -46,21 +49,7 @@ protected byte[] crypt(byte[] input, int cipherMode, String algorithm, String mo
ByteArray key = keyTxt.getText();
ByteArray iv = ivTxt.getText();

SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm);
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
Cipher cipher;
if(algorithm.equals("SM4")){
cipher = Cipher.getInstance(String.format("%s/%s/%s", algorithm, mode, padding), BouncyCastleProvider.PROVIDER_NAME);
}
else{
cipher = Cipher.getInstance(String.format("%s/%s/%s", algorithm, mode, padding));
}

if( mode.equals("ECB") ) {
cipher.init(cipherMode, secretKeySpec);
} else {
cipher.init(cipherMode, secretKeySpec, ivSpec);
}
Cipher cipher = createInitializedCipher(cipherMode, algorithm, mode, padding, key.getBytes(), iv.getBytes());

String selectedInputMode = (String)inputMode.getSelectedItem();
String selectedOutputMode = (String)outputMode.getSelectedItem();
Expand All @@ -80,6 +69,28 @@ protected byte[] crypt(byte[] input, int cipherMode, String algorithm, String mo
return encrypted;
}

protected static Cipher createInitializedCipher(int cipherMode, String algorithm, String mode, String padding,
byte[] key, byte[] iv) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(key, algorithm);
Cipher cipher;
if(algorithm.equals("SM4")){
cipher = Cipher.getInstance(String.format("%s/%s/%s", algorithm, mode, padding), BouncyCastleProvider.PROVIDER_NAME);
}
else{
cipher = Cipher.getInstance(String.format("%s/%s/%s", algorithm, mode, padding));
}

if( mode.equals("ECB") ) {
cipher.init(cipherMode, secretKeySpec);
} else if( mode.equals("GCM") ) {
// GCM requires a GCMParameterSpec; providers reject a plain IvParameterSpec.
cipher.init(cipherMode, secretKeySpec, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
} else {
cipher.init(cipherMode, secretKeySpec, new IvParameterSpec(iv));
}
return cipher;
}

public void createMyUI() {
this.ivTxt = new FormatTextField();
this.ivTxt.addOption("Empty");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package de.usd.cstchef.operations.encryption;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.List;

import org.junit.Test;

import de.usd.cstchef.operations.encryption.CipherUtils.CipherInfo;

public class CipherUtilsTest {

private List<String> modesOf(String algorithm) {
CipherInfo info = CipherUtils.getInstance().getCipherInfo(algorithm);
return Arrays.asList(info.getModes());
}

// Since JDK 9 the providers register GCM only as complete transformations
// (e.g. "Cipher.AES/GCM/NoPadding"), not in "SupportedModes". CipherUtils
// must still surface GCM as a selectable mode.
@Test
public void aesOffersGcmMode() {
assertTrue(modesOf("AES").contains("GCM"));
}

@Test
public void sm4OffersGcmMode() {
assertTrue(modesOf("SM4").contains("GCM"));
}

// The classic modes advertised via "SupportedModes" must survive the merge.
@Test
public void aesKeepsProviderAdvertisedModes() {
List<String> modes = modesOf("AES");
assertTrue(modes.contains("ECB"));
assertTrue(modes.contains("CBC"));
}

// JDK 17 also registers key-wrap transformations ("Cipher.AES/KW/NoPadding").
// Those are not block cipher modes usable by CryptOperation and must not
// leak into the mode combo box.
@Test
public void aesDoesNotOfferKeyWrapModes() {
List<String> modes = modesOf("AES");
assertFalse(modes.contains("KW"));
assertFalse(modes.contains("KWP"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package de.usd.cstchef.operations.encryption;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;

import java.util.Arrays;

import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;

import org.junit.Test;

public class GcmCipherTest {

private static final byte[] KEY_128 = "0123456789abcdef".getBytes();
private static final byte[] NONCE_12 = "0123456789ab".getBytes();
private static final byte[] IV_16 = "0123456789abcdef".getBytes();

private byte[] crypt(int mode, String algorithm, String cipherMode, String padding,
byte[] key, byte[] iv, byte[] input) throws Exception {
Cipher cipher = CryptOperation.createInitializedCipher(mode, algorithm, cipherMode, padding, key, iv);
return cipher.doFinal(input);
}

@Test
public void aesGcmRoundTrip() throws Exception {
byte[] plain = "attack at dawn".getBytes("UTF-8");

byte[] enc = crypt(Cipher.ENCRYPT_MODE, "AES", "GCM", "NOPADDING", KEY_128, NONCE_12, plain);
byte[] dec = crypt(Cipher.DECRYPT_MODE, "AES", "GCM", "NOPADDING", KEY_128, NONCE_12, enc);

assertArrayEquals(plain, dec);
}

// GCM appends a 16 byte authentication tag, so ciphertext != plaintext length.
@Test
public void aesGcmAppendsAuthTag() throws Exception {
byte[] plain = new byte[32];

byte[] enc = crypt(Cipher.ENCRYPT_MODE, "AES", "GCM", "NOPADDING", KEY_128, NONCE_12, plain);

assertFalse(enc.length == plain.length);
assert enc.length == plain.length + 16;
}

@Test(expected = AEADBadTagException.class)
public void aesGcmDetectsTampering() throws Exception {
byte[] plain = "attack at dawn".getBytes("UTF-8");

byte[] enc = crypt(Cipher.ENCRYPT_MODE, "AES", "GCM", "NOPADDING", KEY_128, NONCE_12, plain);
enc[0] ^= 0x01;
crypt(Cipher.DECRYPT_MODE, "AES", "GCM", "NOPADDING", KEY_128, NONCE_12, enc);
}

@Test
public void sm4GcmRoundTrip() throws Exception {
byte[] plain = "attack at dawn".getBytes("UTF-8");

byte[] enc = crypt(Cipher.ENCRYPT_MODE, "SM4", "GCM", "NOPADDING", KEY_128, NONCE_12, plain);
byte[] dec = crypt(Cipher.DECRYPT_MODE, "SM4", "GCM", "NOPADDING", KEY_128, NONCE_12, enc);

assertArrayEquals(plain, dec);
}

// The refactored cipher setup must keep the existing non-AEAD modes working.
@Test
public void aesCbcRoundTripStillWorks() throws Exception {
byte[] plain = "legacy mode".getBytes("UTF-8");

byte[] enc = crypt(Cipher.ENCRYPT_MODE, "AES", "CBC", "PKCS5PADDING", KEY_128, IV_16, plain);
byte[] dec = crypt(Cipher.DECRYPT_MODE, "AES", "CBC", "PKCS5PADDING", KEY_128, IV_16, enc);

assertArrayEquals(plain, dec);
assertFalse(Arrays.equals(plain, enc));
}

@Test
public void aesEcbRoundTripStillWorks() throws Exception {
byte[] plain = "legacy mode".getBytes("UTF-8");

byte[] enc = crypt(Cipher.ENCRYPT_MODE, "AES", "ECB", "PKCS5PADDING", KEY_128, new byte[0], plain);
byte[] dec = crypt(Cipher.DECRYPT_MODE, "AES", "ECB", "PKCS5PADDING", KEY_128, new byte[0], enc);

assertArrayEquals(plain, dec);
}
}