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.hybrid; 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.HybridDecrypt; 25 import com.google.crypto.tink.HybridEncrypt; 26 import com.google.crypto.tink.InsecureSecretKeyAccess; 27 import com.google.crypto.tink.KeyTemplates; 28 import com.google.crypto.tink.KeysetHandle; 29 import com.google.crypto.tink.RegistryConfiguration; 30 import com.google.crypto.tink.TinkJsonProtoKeysetFormat; 31 import com.google.crypto.tink.daead.DeterministicAeadConfig; 32 import com.google.crypto.tink.testing.TestUtil; 33 import java.security.GeneralSecurityException; 34 import org.junit.BeforeClass; 35 import org.junit.experimental.theories.DataPoints; 36 import org.junit.experimental.theories.FromDataPoints; 37 import org.junit.experimental.theories.Theories; 38 import org.junit.experimental.theories.Theory; 39 import org.junit.runner.RunWith; 40 41 /** Unit tests for the Hybrid package. Uses only the public API. */ 42 @RunWith(Theories.class) 43 public final class HybridTest { 44 45 @BeforeClass setUp()46 public static void setUp() throws Exception { 47 HybridConfig.register(); 48 DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonSignatureKeyset_throws. 49 } 50 51 @DataPoints("templates") 52 public static final String[] TEMPLATES = 53 new String[] { 54 "ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM", 55 "ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM_RAW", 56 "ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256_RAW", 57 "ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256", 58 "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW", 59 "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305", 60 "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM", 61 "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM_RAW", 62 "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM" 63 }; 64 65 @Theory createEncryptDecrypt(@romDataPoints"templates") String templateName)66 public void createEncryptDecrypt(@FromDataPoints("templates") String templateName) 67 throws Exception { 68 if (TestUtil.isTsan()) { 69 // KeysetHandle.generateNew is too slow in Tsan. 70 return; 71 } 72 KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName)); 73 KeysetHandle publicHandle = privateHandle.getPublicKeysetHandle(); 74 75 HybridEncrypt encrypter = 76 publicHandle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class); 77 HybridDecrypt decrypter = 78 privateHandle.getPrimitive(RegistryConfiguration.get(), HybridDecrypt.class); 79 80 byte[] plaintext = "plaintext".getBytes(UTF_8); 81 byte[] contextInfo = "contextInfo".getBytes(UTF_8); 82 byte[] ciphertext = encrypter.encrypt(plaintext, contextInfo); 83 assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext); 84 85 KeysetHandle otherPrivateHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName)); 86 HybridDecrypt otherDecrypter = 87 otherPrivateHandle.getPrimitive(RegistryConfiguration.get(), HybridDecrypt.class); 88 assertThrows( 89 GeneralSecurityException.class, () -> otherDecrypter.decrypt(ciphertext, contextInfo)); 90 91 byte[] invalid = "invalid".getBytes(UTF_8); 92 byte[] empty = "".getBytes(UTF_8); 93 assertThrows(GeneralSecurityException.class, () -> decrypter.decrypt(ciphertext, invalid)); 94 assertThrows(GeneralSecurityException.class, () -> decrypter.decrypt(invalid, contextInfo)); 95 assertThrows(GeneralSecurityException.class, () -> decrypter.decrypt(empty, contextInfo)); 96 assertThat(decrypter.decrypt(encrypter.encrypt(empty, contextInfo), contextInfo)) 97 .isEqualTo(empty); 98 assertThat(decrypter.decrypt(encrypter.encrypt(plaintext, empty), empty)).isEqualTo(plaintext); 99 } 100 101 // Keyset with one private key for HybridDecrypt, serialized in Tink's JSON format. 102 private static final String JSON_PRIVATE_KEYSET = "" 103 + "{" 104 + " \"primaryKeyId\": 1885000158," 105 + " \"key\": [" 106 + " {" 107 + " \"keyData\": {" 108 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePrivateKey\"," 109 + " \"value\": \"GiBXM1jmpJqe7HUTTkQxRwEld3bvIPTBhqGcI09ki9H0mRIqGiCwWh0y63G" 110 + "fObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\"," 111 + " \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\"" 112 + " }," 113 + " \"status\": \"ENABLED\"," 114 + " \"keyId\": 1885000158," 115 + " \"outputPrefixType\": \"TINK\"" 116 + " }" 117 + " ]" 118 + "}"; 119 120 // Keyset with the corresponding public key for HybridEncrypt, serialized in Tink's JSON format. 121 private static final String JSON_PUBLIC_KEYSET = "" 122 + "{" 123 + " \"primaryKeyId\": 1885000158," 124 + " \"key\": [" 125 + " {" 126 + " \"keyData\": {" 127 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePublicKey\"," 128 + " \"value\": \"GiCwWh0y63GfObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\"," 129 + " \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\"" 130 + " }," 131 + " \"status\": \"ENABLED\"," 132 + " \"keyId\": 1885000158," 133 + " \"outputPrefixType\": \"TINK\"" 134 + " }" 135 + " ]" 136 + "}"; 137 138 @Theory readKeysetEncryptDecrypt_success()139 public void readKeysetEncryptDecrypt_success() 140 throws Exception { 141 KeysetHandle privateHandle = 142 TinkJsonProtoKeysetFormat.parseKeyset(JSON_PRIVATE_KEYSET, InsecureSecretKeyAccess.get()); 143 KeysetHandle publicHandle = 144 TinkJsonProtoKeysetFormat.parseKeyset(JSON_PUBLIC_KEYSET, InsecureSecretKeyAccess.get()); 145 146 HybridEncrypt encrypter = 147 publicHandle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class); 148 HybridDecrypt decrypter = 149 privateHandle.getPrimitive(RegistryConfiguration.get(), HybridDecrypt.class); 150 151 byte[] plaintext = "plaintext".getBytes(UTF_8); 152 byte[] contextInfo = "contextInfo".getBytes(UTF_8); 153 byte[] ciphertext = encrypter.encrypt(plaintext, contextInfo); 154 assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext); 155 } 156 157 // Keyset with multiple keys. The first key is the same as in JSON_PRIVATE_KEYSET. The second 158 // key is the primary key and will be used for signing. 159 private static final String JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS = 160 "" 161 + "{" 162 + " \"primaryKeyId\": 405658073," 163 + " \"key\": [" 164 + " {" 165 + " \"keyData\": {" 166 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePrivateKey\"," 167 + " \"value\": \"GiBXM1jmpJqe7HUTTkQxRwEld3bvIPTBhqGcI09ki9H0mRIqGiCwWh0y63G" 168 + "fObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\"," 169 + " \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\"" 170 + " }," 171 + " \"status\": \"ENABLED\"," 172 + " \"keyId\": 1885000158," 173 + " \"outputPrefixType\": \"TINK\"" 174 + " }," 175 + " {" 176 + " \"keyData\": {" 177 + " \"typeUrl\":" 178 + "\"type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey\"," 179 + " \"value\": \"GiAGLU3EgraobyU/aOJalcfR2jUUwK/ubd5mTYHIzLHBnBKiASIgJDF8fcN" 180 + "yDS6BcgYpeVPkJ2/ZBG+Mum30OId4D4CzDuQaIP9J2qo487Shr+MxMIkE3VvMro1r4Z+VFoTP3QWVTpz" 181 + "iElwYARJSElAYARISEggQIAoEEBAIAwoGEBAKAggQCjh0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5" 182 + "jcnlwdG8udGluay5BZXNDdHJIbWFjQWVhZEtleQoEEAMIAg==\"," 183 + " \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\"" 184 + " }," 185 + " \"status\": \"ENABLED\"," 186 + " \"keyId\": 405658073," 187 + " \"outputPrefixType\": \"RAW\"" 188 + " }," 189 + " {" 190 + " \"keyData\": {" 191 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePrivateKey\"," 192 + " \"value\": \"GiAnd0VLE8exo149gJ49nkifg03YQLNnRMKfna0AdfYjnBIqGiABOUjRp8F" 193 + "QgppUbZlHCkxRgxGc3jYiChCkm+pf9BL3YhIGGAIQAQgB\"," 194 + " \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\"" 195 + " }," 196 + " \"status\": \"ENABLED\"," 197 + " \"keyId\": 2085058073," 198 + " \"outputPrefixType\": \"LEGACY\"" 199 + " }" 200 + " ]" 201 + "}"; 202 203 // Keyset with the public keys of the keys from JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS. 204 private static final String JSON_PUBLIC_KEYSET_WITH_MULTIPLE_KEYS = 205 "" 206 + "{" 207 + " \"primaryKeyId\": 405658073," 208 + " \"key\": [" 209 + " {" 210 + " \"keyData\": {" 211 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePublicKey\"," 212 + " \"value\": \"GiCwWh0y63GfObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\"," 213 + " \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\"" 214 + " }," 215 + " \"status\": \"ENABLED\"," 216 + " \"keyId\": 1885000158," 217 + " \"outputPrefixType\": \"TINK\"" 218 + " }," 219 + " {" 220 + " \"keyData\": {" 221 + " \"typeUrl\":" 222 + "\"type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey\"," 223 + " \"value\": \"IiAkMXx9w3INLoFyBil5U+Qnb9kEb4y6bfQ4h3gPgLMO5Bog/0naqjjztKG" 224 + "v4zEwiQTdW8yujWvhn5UWhM/dBZVOnOISXBgBElISUBgBEhISCBAgCgQQEAgDCgYQEAoCCBAKOHR5cGU" 225 + "uZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkFlc0N0ckhtYWNBZWFkS2V5CgQQAwgC\"," 226 + " \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\"" 227 + " }," 228 + " \"status\": \"ENABLED\"," 229 + " \"keyId\": 405658073," 230 + " \"outputPrefixType\": \"RAW\"" 231 + " }," 232 + " {" 233 + " \"keyData\": {" 234 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePublicKey\"," 235 + " \"value\": \"GiABOUjRp8FQgppUbZlHCkxRgxGc3jYiChCkm+pf9BL3YhIGGAIQAQgB\"," 236 + " \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\"" 237 + " }," 238 + " \"status\": \"ENABLED\"," 239 + " \"keyId\": 2085058073," 240 + " \"outputPrefixType\": \"LEGACY\"" 241 + " }" 242 + " ]" 243 + "}"; 244 245 @Theory multipleKeysReadKeysetWithEncryptDecrypt()246 public void multipleKeysReadKeysetWithEncryptDecrypt() 247 throws Exception { 248 KeysetHandle privateHandle = 249 TinkJsonProtoKeysetFormat.parseKeyset( 250 JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get()); 251 KeysetHandle publicHandle = 252 TinkJsonProtoKeysetFormat.parseKeyset( 253 JSON_PUBLIC_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get()); 254 255 HybridEncrypt encrypter = 256 publicHandle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class); 257 HybridDecrypt decrypter = 258 privateHandle.getPrimitive(RegistryConfiguration.get(), HybridDecrypt.class); 259 260 byte[] plaintext = "plaintext".getBytes(UTF_8); 261 byte[] contextInfo = "contextInfo".getBytes(UTF_8); 262 byte[] ciphertext = encrypter.encrypt(plaintext, contextInfo); 263 assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext); 264 265 // Also test that decrypter can decrypt ciphertext of a non-primary key. We use 266 // JSON_PUBLIC_KEYSET to create a ciphertext with the first key. 267 KeysetHandle publicHandle1 = 268 TinkJsonProtoKeysetFormat.parseKeyset( 269 JSON_PUBLIC_KEYSET, InsecureSecretKeyAccess.get()); 270 HybridEncrypt encrypter1 = 271 publicHandle1.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class); 272 byte[] ciphertext1 = encrypter1.encrypt(plaintext, contextInfo); 273 assertThat(decrypter.decrypt(ciphertext1, contextInfo)).isEqualTo(plaintext); 274 } 275 276 // A keyset with a valid DeterministicAead key. This keyset can't be used with the HybridEncrypt 277 // or HybridDecrypt. 278 private static final String JSON_DAEAD_KEYSET = 279 "" 280 + "{" 281 + " \"primaryKeyId\": 961932622," 282 + " \"key\": [" 283 + " {" 284 + " \"keyData\": {" 285 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\"," 286 + " \"keyMaterialType\": \"SYMMETRIC\"," 287 + " \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS" 288 + "kvV2+7u6F2DN+kqUjAfkf2W\"" 289 + " }," 290 + " \"outputPrefixType\": \"TINK\"," 291 + " \"keyId\": 961932622," 292 + " \"status\": \"ENABLED\"" 293 + " }" 294 + " ]" 295 + "}"; 296 297 @Theory getPrimitiveFromNonSignatureKeyset_throws()298 public void getPrimitiveFromNonSignatureKeyset_throws() 299 throws Exception { 300 KeysetHandle handle = 301 TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get()); 302 // Test that the keyset can create a DeterministicAead primitive, but neither HybridEncrypt 303 // nor HybridDecrypt primitives. 304 Object unused = handle.getPrimitive(RegistryConfiguration.get(), DeterministicAead.class); 305 assertThrows( 306 GeneralSecurityException.class, 307 () -> handle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class)); 308 assertThrows( 309 GeneralSecurityException.class, 310 () -> handle.getPrimitive(RegistryConfiguration.get(), HybridDecrypt.class)); 311 } 312 } 313