Skip to content

Instantly share code, notes, and snippets.

@reschke
Last active February 19, 2017 18:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save reschke/46659c912b426dffeac41d9a21421c95 to your computer and use it in GitHub Desktop.
Save reschke/46659c912b426dffeac41d9a21421c95 to your computer and use it in GitHub Desktop.
aes128cgm content coding test
import java.io.ByteArrayOutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.junit.Assert;
import org.junit.Test;
public class EncryptionEncodingDecodingDemo {
public static void main(String[] args) throws Exception {
// https://greenbytes.de/tech/webdav/draft-ietf-httpbis-encryption-encoding-07.html#rfc.section.3.1
{
System.out.println("Decoding rs=4096\n");
String body64 = "I1BsxtFttlv3u_Oo94xnmwAAEAAA-NAVub2qFgBEuQKRapoZu-IxkIva3MEB1PD-ly8Thjg";
byte body[] = Base64.getUrlDecoder().decode(body64);
System.out.println(dump(body));
String key64 = "yqdlZ-tYemfogSmv7Ws5PQ";
byte key[] = Base64.getUrlDecoder().decode(key64);
System.out.println(dump(key));
System.out.println(dump(decrypt(key, body)));
System.out.flush();
}
// https://greenbytes.de/tech/webdav/draft-ietf-httpbis-encryption-encoding-07.html#rfc.section.3.2
{
System.out.println("Decoding rs=25\n");
String body64 = "uNCkWiNYzKTnBN9ji3-qWAAAABkCYTHOG8chz_gnvgOqdGYovxyjuqRyJFjEDyoF1Fvkj6hQPdPHI51OEUKEpgz3SsLWIqS_uA";
byte body[] = Base64.getUrlDecoder().decode(body64);
System.out.println(dump(body));
String key64 = "BO3ZVPxUlnLORbVGMpbT1Q";
byte key[] = Base64.getUrlDecoder().decode(key64);
System.out.println(dump(key));
System.out.println(dump(decrypt(key, body)));
System.out.flush();
}
// Encoding tests
{
System.out.println("Encoding rs=4096\n");
byte[] payload = "I am the walrus".getBytes();
System.out.println(dump(payload));
String salt64 = "I1BsxtFttlv3u_Oo94xnmw";
byte salt[] = Base64.getUrlDecoder().decode(salt64);
System.out.println(dump(salt));
String key64 = "yqdlZ-tYemfogSmv7Ws5PQ";
byte key[] = Base64.getUrlDecoder().decode(key64);
System.out.println(dump(key));
byte padding[] = {};
byte[] encrypted = encrypt(key, salt, "", payload, 4096, padding);
System.out.println(dump(encrypted));
System.out.println(Base64.getUrlEncoder().encodeToString(encrypted));
System.out.flush();
}
{
System.out.println("Encoding rs=25\n");
byte[] payload = "I am the walrus".getBytes();
System.out.println(dump(payload));
String salt64 = "uNCkWiNYzKTnBN9ji3-qWA==";
byte salt[] = Base64.getUrlDecoder().decode(salt64);
System.out.println(dump(salt));
String key64 = "BO3ZVPxUlnLORbVGMpbT1Q";
byte key[] = Base64.getUrlDecoder().decode(key64);
System.out.println(dump(key));
byte padding[] = { 1, 0, 0, 0 };
byte[] encrypted = encrypt(key, salt, "a1", payload, 25, padding);
System.out.println(dump(encrypted));
System.out.println(Base64.getUrlEncoder().encodeToString(encrypted));
System.out.flush();
}
}
static class HttpEncHKDF {
private static final String HMAC_ALG = "HmacSHA256";
private final Mac hmacHash;
public HttpEncHKDF(byte[] key, byte[] salt) throws NoSuchAlgorithmException, InvalidKeyException {
Mac thmacHash = Mac.getInstance(HMAC_ALG);
thmacHash.init(new SecretKeySpec(salt, HMAC_ALG));
byte[] prk = thmacHash.doFinal(key);
// System.out.println("PRK: " +
// Base64.getUrlEncoder().encodeToString(prk));
thmacHash.init(new SecretKeySpec(prk, HMAC_ALG));
this.hmacHash = thmacHash;
}
private static final byte HKDF_0 = 0;
private static final byte HKDF_1 = 1;
private static final byte[] CONTEXT = new byte[0];
public byte[] hkdf32(byte[] magic) {
hmacHash.update(magic);
hmacHash.update(HKDF_0);
hmacHash.update(CONTEXT);
hmacHash.update(HKDF_1);
return hmacHash.doFinal();
}
}
private static final int CEK_LEN = 16; // Bytes
private static final int NONCE_LEN = 12; // Bytes
private static final int GCM_TAG_LEN = 16; // Bytes
private static byte[] decrypt(byte[] key, byte[] bytes) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (bytes.length < 21) {
throw new IllegalArgumentException("needs 21 octets of preamble, but got: " + bytes.length);
}
// 16 octets of salt
byte[] salt = Arrays.copyOf(bytes, 16);
int start = 16;
// System.out.println("salt: " +
// Base64.getUrlEncoder().encodeToString(salt));
// 4 octets of record size
int rs = fromNetworkOctets(bytes, start, 4);
start += 4;
if (rs < 18) {
throw new IllegalArgumentException("minimal record size is 18, but got: " + rs);
}
// skip keyid
start += 1 + (bytes[20] & 0xff);
// System.out.println("keylen: " + (bytes[20] & 0xff));
HttpEncHKDF hkdf = new HttpEncHKDF(key, salt);
// https://greenbytes.de/tech/webdav/draft-ietf-httpbis-encryption-encoding-06.html#rfc.section.2.2
byte[] cek = hkdf.hkdf32("Content-Encoding: aes128gcm".getBytes());
// System.out.println("CEK: " +
// Base64.getUrlEncoder().encodeToString(cek));
// https://greenbytes.de/tech/webdav/draft-ietf-httpbis-encryption-encoding-06.html#rfc.section.2.3
byte[] initialNonce = hkdf.hkdf32("Content-Encoding: nonce".getBytes());
// System.out.println("initialNonce: " +
// Base64.getUrlEncoder().encodeToString(initialNonce));
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
byte lastPaddingDelimiter = 0;
// System.err.println("start " + start + " rs " + rs);
for (int i = start, seq = 0; i < bytes.length; i += rs, seq += 1) {
if (lastPaddingDelimiter > 1) {
throw new IllegalArgumentException("unexpected padding delimiter " + lastPaddingDelimiter + " in record " + seq);
}
// compute sequence-number based nonce
byte[] nonce = nonce(initialNonce, seq);
// sanity check on block length
int len = Math.min(rs, bytes.length - i);
if (len < 18) {
throw new IllegalArgumentException("records must have a at least 18 octets");
}
// decode
GCMParameterSpec spec = new GCMParameterSpec(8 * GCM_TAG_LEN, nonce, 0, NONCE_LEN);
SecretKey skey = new SecretKeySpec(cek, 0, CEK_LEN, "AES");
cipher.init(Cipher.DECRYPT_MODE, skey, spec);
byte[] plainText = cipher.doFinal(bytes, i, len);
// scan backwards for padding delimiter
byte paddingDelimiter = 0;
int p;
for (p = plainText.length - 1; p >= 0 && paddingDelimiter == 0; p--) {
byte b = plainText[p];
if (b == 0) {
// padding
} else if (b == 1 || b == 2) {
paddingDelimiter = b;
}
}
// System.err.println(seq + " padding " + p + " delim " +
// paddingDelimiter);
// append octets to output
bos.write(plainText, 0, p + 1);
lastPaddingDelimiter = paddingDelimiter;
}
if (lastPaddingDelimiter != 2) {
throw new IllegalArgumentException("unexpected padding delimiter " + lastPaddingDelimiter + " in last record");
}
return bos.toByteArray();
}
private static byte[] encrypt(byte[] key, byte[] salt, String keyid, byte[] bytes, int rs, byte[] padding) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HttpEncHKDF hkdf = new HttpEncHKDF(key, salt);
// https://greenbytes.de/tech/webdav/draft-ietf-httpbis-encryption-encoding-06.html#rfc.section.2.2
byte[] cek = hkdf.hkdf32("Content-Encoding: aes128gcm".getBytes());
// https://greenbytes.de/tech/webdav/draft-ietf-httpbis-encryption-encoding-06.html#rfc.section.2.3
byte[] initialNonce = hkdf.hkdf32("Content-Encoding: nonce".getBytes());
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
if (padding == null || padding.length == 0) {
padding = new byte[] { 0 };
}
// insert header block
bos.write(salt, 0, salt.length);
bos.write(toNetworkOctets(rs, 4), 0, 4);
bos.write((byte) (keyid.getBytes("UTF-8").length));
bos.write(keyid.getBytes("UTF-8"));
rs -= GCM_TAG_LEN;
for (int i = 0, seq = 0; i < bytes.length; seq += 1) {
int padbytes = padding[seq % padding.length];
// compute sequence-number based nonce
byte[] nonce = nonce(initialNonce, seq);
GCMParameterSpec spec = new GCMParameterSpec(8 * GCM_TAG_LEN, nonce, 0, NONCE_LEN);
SecretKey skey = new SecretKeySpec(cek, 0, CEK_LEN, "AES");
cipher.init(Cipher.ENCRYPT_MODE, skey, spec);
int dlen = Math.min(rs - 1 - padbytes, bytes.length - i);
// System.err.println("seq " + seq + " i " + i + " padbytes " +
// padbytes + " dlen " + dlen);
byte encrypted[] = null;
byte[] input = new byte[rs];
System.arraycopy(bytes, i, input, 0, dlen);
i += (rs - padbytes - 1);
input[dlen] = (i >= bytes.length) ? (byte) 2 : (byte) 1;
encrypted = cipher.doFinal(input, 0, dlen + 1 + padbytes);
// System.err.println("seq " + seq + " bytes " + encrypted.length);
bos.write(encrypted);
}
return bos.toByteArray();
}
/**
* Convert octets (network byte order) to integer
*/
private static int fromNetworkOctets(byte[] octets, int pos, int len) {
int result = 0;
for (int i = 0; i < len; i++) {
result *= 256;
result += octets[pos + i] & 0xff;
}
return result;
}
/**
* Convert integer into n octet binary representation in network byte order
*/
private static byte[] toNetworkOctets(long in, int amount) {
byte[] result = new byte[amount];
for (int i = result.length - 1; i >= 0; i -= 1) {
result[i] = (byte) (in & 0xff);
in >>= 8;
}
return result;
}
/**
* Compute nonce based on initialNonce and sequence number
*/
private static byte[] nonce(byte[] initialNonce, int seq) {
byte[] result = new byte[12];
byte[] seqbytes = toNetworkOctets(seq, 12);
for (int i = 0; i < result.length; i++) {
result[i] = (byte) (initialNonce[i] ^ seqbytes[i]);
}
return result;
}
/**
* Convert octet array to printable string
*/
private static String dump(byte bytes[]) {
StringBuilder dump = new StringBuilder();
for (int i = 0; i < bytes.length; i += 16) {
StringBuilder hex = new StringBuilder();
StringBuilder ascii = new StringBuilder();
for (int j = i; j < i + 16; j++) {
if (j < bytes.length) {
byte b = bytes[j];
hex.append(String.format("%02x ", b));
if (b >= 32 && b < 126) {
ascii.append(String.format("%c", b));
} else {
ascii.append("_");
}
} else {
hex.append(" ");
}
}
dump.append(hex).append(" ").append(ascii).append("\n");
}
return dump.toString();
}
// Unit tests
@Test
public void testOctetDecoding() {
Assert.assertEquals(0, fromNetworkOctets(new byte[] { 0, 0 }, 0, 2));
Assert.assertEquals(256, fromNetworkOctets(new byte[] { 1, 0 }, 0, 2));
Assert.assertEquals(65535, fromNetworkOctets(new byte[] { (byte) 255, (byte) 255 }, 0, 2));
}
@Test
public void testOctetEncoding() {
Assert.assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, toNetworkOctets(0, 12));
Assert.assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, toNetworkOctets(1, 12));
Assert.assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 2, 1 }, toNetworkOctets(1 + 2 * 256 + 3 * 65536, 12));
Assert.assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 127, (byte) 255, (byte) 255, (byte) 255 },
toNetworkOctets(Integer.MAX_VALUE, 12));
Assert.assertArrayEquals(
new byte[] { 0, 0, 0, 0, 127, (byte) 255, (byte) 255, (byte) 255, (byte) 255, (byte) 255, (byte) 255, (byte) 255 },
toNetworkOctets(Long.MAX_VALUE, 12));
}
@Test
public void testSpec1() throws Exception {
String key64 = "yqdlZ-tYemfogSmv7Ws5PQ";
byte key[] = Base64.getUrlDecoder().decode(key64);
byte[] payload = "I am the walrus".getBytes();
String salt64 = "I1BsxtFttlv3u_Oo94xnmw";
byte salt[] = Base64.getUrlDecoder().decode(salt64);
String body64 = "I1BsxtFttlv3u_Oo94xnmwAAEAAA-NAVub2qFgBEuQKRapoZu-IxkIva3MEB1PD-ly8Thjg";
byte body[] = Base64.getUrlDecoder().decode(body64);
byte padding[] = {};
Assert.assertArrayEquals(body, encrypt(key, salt, "", payload, 4096, padding));
byte result[] = decrypt(key, body);
Assert.assertArrayEquals(payload, result);
}
@Test
public void testSpec2() throws Exception {
String key64 = "BO3ZVPxUlnLORbVGMpbT1Q";
byte key[] = Base64.getUrlDecoder().decode(key64);
byte[] payload = "I am the walrus".getBytes();
String salt64 = "uNCkWiNYzKTnBN9ji3-qWA==";
byte salt[] = Base64.getUrlDecoder().decode(salt64);
String body64 = "uNCkWiNYzKTnBN9ji3-qWAAAABkCYTHOG8chz_gnvgOqdGYovxyjuqRyJFjEDyoF1Fvkj6hQPdPHI51OEUKEpgz3SsLWIqS_uA";
byte body[] = Base64.getUrlDecoder().decode(body64);
byte padding[] = { 1, 0, 0, 0 };
Assert.assertArrayEquals(body, encrypt(key, salt, "a1", payload, 25, padding));
byte result[] = decrypt(key, body);
Assert.assertArrayEquals(payload, result);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment