1 // Copyright 2022 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink.streamingaead; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static java.nio.charset.StandardCharsets.UTF_8; 21 import static org.junit.Assert.assertThrows; 22 23 import com.google.crypto.tink.DeterministicAead; 24 import com.google.crypto.tink.InsecureSecretKeyAccess; 25 import com.google.crypto.tink.KeyTemplates; 26 import com.google.crypto.tink.KeysetHandle; 27 import com.google.crypto.tink.StreamingAead; 28 import com.google.crypto.tink.TinkJsonProtoKeysetFormat; 29 import com.google.crypto.tink.daead.DeterministicAeadConfig; 30 import java.io.ByteArrayInputStream; 31 import java.io.ByteArrayOutputStream; 32 import java.io.IOException; 33 import java.nio.ByteBuffer; 34 import java.nio.channels.Channels; 35 import java.nio.channels.ReadableByteChannel; 36 import java.nio.channels.WritableByteChannel; 37 import java.security.GeneralSecurityException; 38 import org.junit.BeforeClass; 39 import org.junit.experimental.theories.DataPoints; 40 import org.junit.experimental.theories.FromDataPoints; 41 import org.junit.experimental.theories.Theories; 42 import org.junit.experimental.theories.Theory; 43 import org.junit.runner.RunWith; 44 45 /** Unit tests for the StreamingAead package. Uses only the public API. */ 46 @RunWith(Theories.class) 47 public final class StreamingAeadTest { 48 49 @BeforeClass setUp()50 public static void setUp() throws Exception { 51 StreamingAeadConfig.register(); 52 DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonStreamingAeadKeyset_throws. 53 } 54 55 @DataPoints("templates") 56 public static final String[] TEMPLATES = 57 new String[] { 58 "AES128_GCM_HKDF_4KB", 59 "AES128_GCM_HKDF_1MB", 60 "AES256_GCM_HKDF_4KB", 61 "AES256_GCM_HKDF_1MB", 62 "AES128_CTR_HMAC_SHA256_4KB", 63 "AES128_CTR_HMAC_SHA256_1MB", 64 "AES256_CTR_HMAC_SHA256_4KB", 65 "AES256_CTR_HMAC_SHA256_1MB" 66 }; 67 68 /** Writes {@code data} to {@code writeableChannel}. */ writeToChannel(WritableByteChannel writeableChannel, byte[] data)69 private void writeToChannel(WritableByteChannel writeableChannel, byte[] data) 70 throws IOException { 71 ByteBuffer buffer = ByteBuffer.wrap(data); 72 int bytesWritten = 0; 73 while (bytesWritten < data.length) { 74 bytesWritten += writeableChannel.write(buffer); 75 } 76 } 77 78 /** Reads {@code bytesToRead} bytes from {@code readableChannel}.*/ readFromChannel(ReadableByteChannel readableChannel, int bytesToRead)79 private byte[] readFromChannel(ReadableByteChannel readableChannel, int bytesToRead) 80 throws IOException { 81 ByteBuffer buffer = ByteBuffer.allocate(bytesToRead); 82 int bytesRead = 0; 83 while (bytesRead < bytesToRead) { 84 bytesRead += readableChannel.read(buffer); 85 } 86 return buffer.array(); 87 } 88 89 @Theory createEncryptDecrypt(@romDataPoints"templates") String templateName)90 public void createEncryptDecrypt(@FromDataPoints("templates") String templateName) 91 throws Exception { 92 KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get(templateName)); 93 StreamingAead streamingAead = handle.getPrimitive(StreamingAead.class); 94 95 byte[] plaintext = "plaintext".getBytes(UTF_8); 96 byte[] associatedData = "associatedData".getBytes(UTF_8); 97 98 // Encrypt 99 ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream(); 100 try (WritableByteChannel encryptingChannel = 101 streamingAead.newEncryptingChannel( 102 Channels.newChannel(ciphertextOutputStream), associatedData)) { 103 writeToChannel(encryptingChannel, plaintext); 104 } 105 byte[] ciphertext = ciphertextOutputStream.toByteArray(); 106 107 // Decrypt 108 byte[] decrypted = null; 109 ReadableByteChannel ciphertextSource = 110 Channels.newChannel(new ByteArrayInputStream(ciphertext)); 111 try (ReadableByteChannel decryptingChannel = 112 streamingAead.newDecryptingChannel(ciphertextSource, associatedData)) { 113 decrypted = readFromChannel(decryptingChannel, plaintext.length); 114 } 115 assertThat(decrypted).isEqualTo(plaintext); 116 117 // Decrypt with invalid associatedData fails 118 byte[] invalidAssociatedData = "invalid".getBytes(UTF_8); 119 ByteBuffer decrypted2 = ByteBuffer.allocate(plaintext.length); 120 ReadableByteChannel ciphertextSource2 = 121 Channels.newChannel(new ByteArrayInputStream(ciphertext)); 122 try (ReadableByteChannel decryptingChannel = 123 streamingAead.newDecryptingChannel(ciphertextSource2, invalidAssociatedData)) { 124 assertThrows(IOException.class, () -> decryptingChannel.read(decrypted2)); 125 } 126 } 127 128 // A keyset with one StreamingAead key, serialized in Tink's JSON format. 129 private static final String JSON_STREAMING_AEAD_KEYSET = 130 "" 131 + "{" 132 + " \"primaryKeyId\": 1261393457," 133 + " \"key\": [" 134 + " {" 135 + " \"keyData\": {" 136 + " \"typeUrl\":" 137 + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\"," 138 + " \"value\": \"GhBqEXuGvNvVCRjJX1IMvC0kEg4iBBAgCAMYAxAQCICAQA==\"," 139 + " \"keyMaterialType\": \"SYMMETRIC\"" 140 + " }," 141 + " \"status\": \"ENABLED\"," 142 + " \"keyId\": 1261393457," 143 + " \"outputPrefixType\": \"RAW\"" 144 + " }" 145 + " ]" 146 + "}"; 147 148 @Theory readKeyset_encryptDecrypt_success()149 public void readKeyset_encryptDecrypt_success() throws Exception { 150 KeysetHandle handle = 151 TinkJsonProtoKeysetFormat.parseKeyset( 152 JSON_STREAMING_AEAD_KEYSET, InsecureSecretKeyAccess.get()); 153 StreamingAead streamingAead = handle.getPrimitive(StreamingAead.class); 154 155 byte[] plaintext = "plaintext".getBytes(UTF_8); 156 byte[] associatedData = "associatedData".getBytes(UTF_8); 157 158 ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream(); 159 try (WritableByteChannel encryptingChannel = 160 streamingAead.newEncryptingChannel( 161 Channels.newChannel(ciphertextOutputStream), associatedData)) { 162 writeToChannel(encryptingChannel, plaintext); 163 } 164 byte[] ciphertext = ciphertextOutputStream.toByteArray(); 165 166 byte[] decrypted = null; 167 ReadableByteChannel ciphertextSource = 168 Channels.newChannel(new ByteArrayInputStream(ciphertext)); 169 try (ReadableByteChannel decryptingChannel = 170 streamingAead.newDecryptingChannel(ciphertextSource, associatedData)) { 171 decrypted = readFromChannel(decryptingChannel, plaintext.length); 172 } 173 assertThat(decrypted).isEqualTo(plaintext); 174 } 175 176 // A keyset with multiple keys. The first key is the same as in JSON_STREAMING_AEAD_KEYSET. 177 private static final String JSON_STREAMING_AEAD_KEYSET_WITH_MULTIPLE_KEYS = 178 "" 179 + "{" 180 + " \"primaryKeyId\": 1539463392," 181 + " \"key\": [" 182 + " {" 183 + " \"keyData\": {" 184 + " \"typeUrl\":" 185 + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\"," 186 + " \"value\": \"GhBqEXuGvNvVCRjJX1IMvC0kEg4iBBAgCAMYAxAQCICAQA==\"," 187 + " \"keyMaterialType\": \"SYMMETRIC\"" 188 + " }," 189 + " \"status\": \"ENABLED\"," 190 + " \"keyId\": 1261393457," 191 + " \"outputPrefixType\": \"RAW\"" 192 + " }," 193 + " {" 194 + " \"keyData\": {" 195 + " \"typeUrl\":" 196 + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\"," 197 + " \"value\":" 198 + "\"GiA33jWXeuaAVvmFGQdU71KKA1K0rUQV8moj5LupxpgCJRIOIgQQIAgDGAMQIAiAgEA=\"," 199 + " \"keyMaterialType\": \"SYMMETRIC\"" 200 + " }," 201 + " \"status\": \"ENABLED\"," 202 + " \"keyId\": 1539463392," 203 + " \"outputPrefixType\": \"RAW\"" 204 + " }," 205 + " {" 206 + " \"keyData\": {" 207 + " \"typeUrl\":" 208 + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\"," 209 + " \"value\":" 210 + "\"GiBpie88LEnKNiGNtyDiUwDmDzeHgrpmf4k2tC1OaWUpcRIOIgQQIAgDGAMQIAiAgEA=\"," 211 + " \"keyMaterialType\": \"SYMMETRIC\"" 212 + " }," 213 + " \"status\": \"ENABLED\"," 214 + " \"keyId\": 552736913," 215 + " \"outputPrefixType\": \"RAW\"" 216 + " }" 217 + " ]" 218 + "}"; 219 220 @Theory multipleKeysReadKeyset_encryptDecrypt_success()221 public void multipleKeysReadKeyset_encryptDecrypt_success() throws Exception { 222 KeysetHandle handle = 223 TinkJsonProtoKeysetFormat.parseKeyset( 224 JSON_STREAMING_AEAD_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get()); 225 StreamingAead streamingAead = handle.getPrimitive(StreamingAead.class); 226 227 byte[] plaintext = "plaintext".getBytes(UTF_8); 228 byte[] associatedData = "associatedData".getBytes(UTF_8); 229 230 ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream(); 231 try (WritableByteChannel encryptingChannel = 232 streamingAead.newEncryptingChannel( 233 Channels.newChannel(ciphertextOutputStream), associatedData)) { 234 writeToChannel(encryptingChannel, plaintext); 235 } 236 byte[] ciphertext = ciphertextOutputStream.toByteArray(); 237 238 byte[] decrypted = null; 239 ReadableByteChannel ciphertextSource = 240 Channels.newChannel(new ByteArrayInputStream(ciphertext)); 241 try (ReadableByteChannel decryptingChannel = 242 streamingAead.newDecryptingChannel(ciphertextSource, associatedData)) { 243 decrypted = readFromChannel(decryptingChannel, plaintext.length); 244 } 245 assertThat(decrypted).isEqualTo(plaintext); 246 } 247 248 // A keyset with a valid DeterministicAead key. This keyset can't be used with the StreamingAead 249 // primitive. 250 private static final String JSON_DAEAD_KEYSET = 251 "" 252 + "{" 253 + " \"primaryKeyId\": 961932622," 254 + " \"key\": [" 255 + " {" 256 + " \"keyData\": {" 257 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\"," 258 + " \"keyMaterialType\": \"SYMMETRIC\"," 259 + " \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS" 260 + "kvV2+7u6F2DN+kqUjAfkf2W\"" 261 + " }," 262 + " \"outputPrefixType\": \"TINK\"," 263 + " \"keyId\": 961932622," 264 + " \"status\": \"ENABLED\"" 265 + " }" 266 + " ]" 267 + "}"; 268 269 @Theory getPrimitiveFromNonStreamingAeadKeyset_throws()270 public void getPrimitiveFromNonStreamingAeadKeyset_throws() 271 throws Exception { 272 KeysetHandle handle = 273 TinkJsonProtoKeysetFormat.parseKeyset( 274 JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get()); 275 // Test that the keyset can create a DeterministicAead primitive, but not a StreamingAead. 276 Object unused = handle.getPrimitive(DeterministicAead.class); 277 assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(StreamingAead.class)); 278 } 279 } 280