1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package libcore.android.crypto.hpke; 17 18 import static android.crypto.hpke.AeadParameterSpec.AES_128_GCM; 19 import static android.crypto.hpke.AeadParameterSpec.AES_256_GCM; 20 import static android.crypto.hpke.AeadParameterSpec.CHACHA20POLY1305; 21 import static android.crypto.hpke.KdfParameterSpec.HKDF_SHA256; 22 import static android.crypto.hpke.KemParameterSpec.DHKEM_X25519_HKDF_SHA256; 23 24 import static org.junit.Assert.assertArrayEquals; 25 import static org.junit.Assert.assertEquals; 26 import static org.junit.Assert.assertNotEquals; 27 import static org.junit.Assert.assertNotNull; 28 import static org.junit.Assert.assertThrows; 29 30 import android.crypto.hpke.AeadParameterSpec; 31 import android.crypto.hpke.Hpke; 32 import android.crypto.hpke.Message; 33 import android.crypto.hpke.Recipient; 34 import android.crypto.hpke.Sender; 35 36 import libcore.util.NonNull; 37 38 import org.junit.Before; 39 import org.junit.Test; 40 import org.junit.experimental.runners.Enclosed; 41 import org.junit.runner.RunWith; 42 import org.junit.runners.JUnit4; 43 import org.junit.runners.Parameterized; 44 import org.junit.runners.Parameterized.Parameter; 45 import org.junit.runners.Parameterized.Parameters; 46 47 import java.nio.charset.StandardCharsets; 48 import java.security.InvalidKeyException; 49 import java.security.KeyPair; 50 import java.security.KeyPairGenerator; 51 import java.security.NoSuchAlgorithmException; 52 import java.security.NoSuchProviderException; 53 import java.security.PrivateKey; 54 import java.security.Provider; 55 import java.security.PublicKey; 56 import java.security.Security; 57 import java.util.List; 58 59 @RunWith(Enclosed.class) 60 public class HpkeTest { 61 62 @RunWith(Parameterized.class) 63 public static class SendReceiveTests { 64 private PublicKey publicKey; 65 private PrivateKey privateKey; 66 67 private static final byte[] EMPTY = new byte[0]; 68 private static final byte[] MESSAGE = "This is only a test".getBytes(StandardCharsets.US_ASCII); 69 private static final byte[] INFO = "App info".getBytes(StandardCharsets.US_ASCII); 70 private static final byte[] AAD = "Additional data".getBytes(StandardCharsets.US_ASCII); 71 72 @Parameters data()73 public static Object[][] data() { 74 Object[] aads = new Object[]{null, EMPTY, AAD}; 75 Object[] infos = new Object[]{null, EMPTY, INFO}; 76 Object[] messages = new Object[]{EMPTY, MESSAGE}; 77 Object[][] ciphers = new Object[][]{ 78 {AES_128_GCM}, 79 {AES_256_GCM}, 80 {CHACHA20POLY1305} 81 }; 82 return permute(aads, permute(infos, permute(messages, ciphers))); 83 } 84 85 @Parameter() 86 public byte[] aad; 87 88 @Parameter(1) 89 public byte[] info; 90 91 @Parameter(2) 92 public byte[] plaintext; 93 94 @Parameter(3) 95 public AeadParameterSpec aead; 96 97 private String suiteName; 98 99 @Before before()100 public void before() throws Exception { 101 KeyPairGenerator generator = KeyPairGenerator.getInstance("XDH"); 102 KeyPair pair = generator.generateKeyPair(); 103 publicKey = pair.getPublic(); 104 privateKey = pair.getPrivate(); 105 suiteName = Hpke.getSuiteName(DHKEM_X25519_HKDF_SHA256, HKDF_SHA256, aead); 106 assertNotNull(suiteName); 107 } 108 109 @Test sendMessage()110 public void sendMessage() throws Exception { 111 Hpke hpke = Hpke.getInstance(suiteName); 112 assertNotNull(hpke); 113 Sender.Builder senderBuilder = new Sender.Builder(hpke, publicKey); 114 if (info != null) { 115 senderBuilder.setApplicationInfo(info); 116 } 117 Sender sender = senderBuilder.build(); 118 byte[] ciphertext = sender.seal(plaintext, aad); 119 byte[] encapsulated = sender.getEncapsulated(); 120 assertNotNull(encapsulated); 121 122 Recipient.Builder recipientBuilder = 123 new Recipient.Builder(hpke, encapsulated, privateKey); 124 if (info != null) { 125 recipientBuilder.setApplicationInfo(info); 126 } 127 Recipient recipient = recipientBuilder.build(); 128 byte[] decoded = recipient.open(ciphertext, aad); 129 assertNotNull(decoded); 130 131 assertArrayEquals(plaintext, decoded); 132 } 133 134 @Test oneshot()135 public void oneshot() throws Exception { 136 Hpke hpke = Hpke.getInstance(suiteName); 137 Message message = hpke.seal(publicKey, info, plaintext, aad); 138 byte[] decoded = hpke.open(privateKey, info, message, aad); 139 assertArrayEquals(plaintext, decoded); 140 } 141 142 // Permute a new set of values into an existing Parameters array, i.e. one new row 143 // is created for every combination of each new value and existing row. permute(Object[] newValues, Object[][] existing)144 private static Object[][] permute(Object[] newValues, Object[][] existing) { 145 int newSize = newValues.length * existing.length; 146 int rowSize = existing[0].length + 1; 147 Object[][] result = new Object[newSize][]; 148 for (int i = 0; i < newSize; i++) { 149 Object[] row = new Object[rowSize]; 150 result[i] = row; 151 row[0] = newValues[i % newValues.length]; 152 System.arraycopy(existing[i / newValues.length], 0, row, 1, rowSize - 1); 153 } 154 return result; 155 } 156 } 157 158 @RunWith(JUnit4.class) 159 public static class OtherTests { 160 private static final String SUITE_NAME = "DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/AES_128_GCM"; 161 private static final int EXPORT_LENGTH = 16; 162 private static final String CONSCRYPT_NAME = "AndroidOpenSSL"; 163 private final Provider conscrypt = Security.getProvider(CONSCRYPT_NAME); 164 165 private PublicKey publicKey; 166 private PrivateKey privateKey; 167 168 @Before before()169 public void before() throws Exception { 170 KeyPairGenerator generator = KeyPairGenerator.getInstance("XDH"); 171 KeyPair pair = generator.generateKeyPair(); 172 publicKey = pair.getPublic(); 173 privateKey = pair.getPrivate(); 174 } 175 176 @Test init_Errors()177 public void init_Errors() { 178 assertThrows(NoSuchAlgorithmException.class, 179 () ->Hpke.getInstance("No such")); 180 assertThrows(NoSuchAlgorithmException.class, 181 () ->Hpke.getInstance("")); 182 assertThrows(NoSuchAlgorithmException.class, 183 () ->Hpke.getInstance(null)); 184 185 assertThrows(IllegalArgumentException.class, 186 () ->Hpke.getInstance(SUITE_NAME, (String) null)); 187 assertThrows(IllegalArgumentException.class, 188 () ->Hpke.getInstance(SUITE_NAME, "")); 189 assertThrows(NoSuchProviderException.class, 190 () ->Hpke.getInstance(SUITE_NAME, "No such")); 191 assertThrows(NoSuchAlgorithmException.class, 192 () ->Hpke.getInstance("No such", CONSCRYPT_NAME)); 193 assertThrows(NoSuchAlgorithmException.class, 194 () ->Hpke.getInstance("", CONSCRYPT_NAME)); 195 assertThrows(NoSuchAlgorithmException.class, 196 () ->Hpke.getInstance(null, CONSCRYPT_NAME)); 197 198 assertThrows(IllegalArgumentException.class, 199 () ->Hpke.getInstance(SUITE_NAME, (Provider) null)); 200 assertThrows(NoSuchAlgorithmException.class, 201 () ->Hpke.getInstance("No such", conscrypt)); 202 assertThrows(NoSuchAlgorithmException.class, 203 () ->Hpke.getInstance("", conscrypt)); 204 assertThrows(NoSuchAlgorithmException.class, 205 () ->Hpke.getInstance(null, conscrypt)); 206 } 207 208 @Test keyType()209 public void keyType() throws Exception { 210 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); 211 KeyPair pair = generator.generateKeyPair(); 212 PublicKey publicRsa = pair.getPublic(); 213 PrivateKey privateRsa = pair.getPrivate(); 214 Hpke hpke = Hpke.getInstance(SUITE_NAME); 215 216 assertThrows(InvalidKeyException.class, 217 () -> new Sender.Builder(hpke, publicRsa).build()); 218 assertThrows(InvalidKeyException.class, 219 () -> new Recipient.Builder(hpke, new byte[16], privateRsa).build()); 220 } 221 222 @Test suiteNames()223 public void suiteNames() throws Exception { 224 List<AeadParameterSpec> aeads = List.of(AES_128_GCM, AES_256_GCM, CHACHA20POLY1305); 225 for (AeadParameterSpec aead : aeads) { 226 String suiteName 227 = Hpke.getSuiteName(DHKEM_X25519_HKDF_SHA256, HKDF_SHA256, aead); 228 assertNotNull(suiteName); 229 assertNotNull(Hpke.getInstance(suiteName)); 230 // Also check Tink-compatible names 231 // TODO(prb) enable after https://github.com/google/conscrypt/pull/1258 lands 232 // String altName = suiteName.replaceAll("/", "_"); 233 // assertNotNull(Hpke.getInstance(altName)); 234 } 235 } 236 237 // Note API test only, implementation tests are in Conscrypt. 238 @Test export()239 public void export() throws Exception { 240 byte[] context = "Hello".getBytes(StandardCharsets.UTF_8); 241 Hpke hpke = Hpke.getInstance(SUITE_NAME); 242 assertNotNull(hpke); 243 Sender sender = new Sender.Builder(hpke, publicKey).build(); 244 Recipient recipient = 245 new Recipient.Builder(hpke, sender.getEncapsulated(), privateKey).build(); 246 assertNotNull(recipient); 247 248 byte[] senderData = sender.export(EXPORT_LENGTH, context); 249 assertNotNull(senderData); 250 assertEquals(EXPORT_LENGTH, senderData.length); 251 int sum = 0; 252 for (byte b : senderData) { 253 sum += b; 254 } 255 // Check data isn't all zeroes. 256 assertNotEquals(0, sum); 257 258 byte[] recipientData = recipient.export(EXPORT_LENGTH, context); 259 assertArrayEquals(senderData, recipientData); 260 } 261 262 @Test spiAndProvider()263 public void spiAndProvider() throws Exception{ 264 Hpke hpke = Hpke.getInstance(SUITE_NAME); 265 assertNotNull(hpke); 266 assertNotNull(hpke.getProvider()); 267 268 Sender sender = new Sender.Builder(hpke, publicKey).build(); 269 Recipient recipient = 270 new Recipient.Builder(hpke, sender.getEncapsulated(), privateKey).build(); 271 assertNotNull(recipient); 272 273 assertNotNull(sender.getProvider()); 274 assertNotNull(sender.getSpi()); 275 assertNotNull(recipient.getProvider()); 276 assertNotNull(recipient.getSpi()); 277 } 278 279 // Note API test only. Implementation not yet present. 280 @Test futureBuilderMethods()281 public void futureBuilderMethods() throws Exception { 282 byte[] appInfo = "App Info".getBytes(StandardCharsets.UTF_8); 283 byte[] psk = "Very Secret Key".getBytes(StandardCharsets.UTF_8); 284 byte[] pskId = "ID".getBytes(StandardCharsets.UTF_8); 285 286 Hpke hpke = Hpke.getInstance(SUITE_NAME); 287 assertNotNull(hpke); 288 289 Sender.Builder senderBuilder = new Sender.Builder(hpke, publicKey) 290 .setApplicationInfo(appInfo) 291 .setSenderKey(privateKey) 292 .setPsk(psk, pskId); 293 assertThrows(UnsupportedOperationException.class, senderBuilder::build); 294 295 Sender sender = new Sender.Builder(hpke, publicKey).build(); 296 assertNotNull(sender); 297 298 Recipient.Builder recipientBuilder = 299 new Recipient.Builder(hpke, sender.getEncapsulated(), privateKey) 300 .setApplicationInfo(appInfo) 301 .setSenderKey(publicKey) 302 .setPsk(psk, pskId); 303 assertThrows(UnsupportedOperationException.class, recipientBuilder::build); 304 } 305 } 306 }