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