Last active
February 19, 2017 18:10
-
-
Save reschke/46659c912b426dffeac41d9a21421c95 to your computer and use it in GitHub Desktop.
aes128cgm content coding test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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