1 /* 2 * Copyright (C) 2023 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 17 package com.android.adservices.ohttp; 18 19 import android.annotation.NonNull; 20 21 import com.android.adservices.ohttp.algorithms.AeadAlgorithmSpec; 22 import com.android.adservices.ohttp.algorithms.KdfAlgorithmSpec; 23 import com.android.adservices.ohttp.algorithms.KemAlgorithmSpec; 24 import com.android.adservices.ohttp.algorithms.UnsupportedHpkeAlgorithmException; 25 26 import com.google.common.base.Preconditions; 27 28 import java.io.ByteArrayOutputStream; 29 import java.io.DataOutputStream; 30 import java.io.IOException; 31 import java.nio.charset.StandardCharsets; 32 import java.security.SecureRandom; 33 import java.util.Arrays; 34 35 /** Provides methods for OHTTP server/gateway side encryption and decryption */ 36 // TODO(b/309955907): Refactor ObliviousHttpGateway and ObliviousHttpClient to reduce duplication 37 public class ObliviousHttpGateway { 38 // HPKE export methods require context strings to export aead key and nonce as defined in 39 // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-3 40 private static final String AEAD_KEY_CONTEXT = "key"; 41 private static final String AEAD_NONCE_CONTEXT = "nonce"; 42 43 /** 44 * According to https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-6 45 * 46 * <p>hdr = concat(encode(1, keyID), encode(2, kemID), encode(2, kdfID), encode(2, aeadID)) 47 * 48 * <p>total length = 7 (1+2+2+2) 49 */ 50 private static final int MESSAGE_HEADER_LENGTH_IN_BYTES = 7; 51 52 /** 53 * Decrypts the given encapsulated request using the private key provided 54 * 55 * <p>From https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4-4 56 * 57 * <p>The encapsulated request is a combination of : 58 * 59 * <pre> Encapsulated Request { 60 * Key Identifier (8), 61 * KEM Identifier (16), 62 * KDF Identifier (16), 63 * AEAD Identifier (16), 64 * Encapsulated KEM Shared Secret (8*Nenc), 65 * AEAD-Protected Request (..)} 66 * </pre> 67 * 68 * <p>This method provides a way to decrypt payloads generated by {@link 69 * ObliviousHttpClient#createObliviousHttpRequest(byte[], boolean)} 70 * 71 * @param privateKey The private key with which to decrypt the payload 72 * @param encapsulatedRequest The payload to decrypt. 73 * @return the decrypted bytes 74 */ decrypt( OhttpGatewayPrivateKey privateKey, @NonNull byte[] encapsulatedRequest)75 public static byte[] decrypt( 76 OhttpGatewayPrivateKey privateKey, @NonNull byte[] encapsulatedRequest) 77 throws UnsupportedHpkeAlgorithmException, IOException { 78 boolean isMediaTypeChanged = false; 79 KemAlgorithmSpec kemAlgorithmSpec; 80 KdfAlgorithmSpec kdfAlgorithmSpec; 81 AeadAlgorithmSpec aeadAlgorithmSpec; 82 83 // Parse the encapsulated request into its components 84 try { 85 kemAlgorithmSpec = getKem(encapsulatedRequest); 86 kdfAlgorithmSpec = getKdf(encapsulatedRequest); 87 aeadAlgorithmSpec = getAead(encapsulatedRequest); 88 } catch (UnsupportedHpkeAlgorithmException e) { 89 // Remove version byte in case is using message/auction format (see 90 // go/fledge-ohttp-media-type-update) 91 isMediaTypeChanged = true; 92 encapsulatedRequest = 93 Arrays.copyOfRange(encapsulatedRequest, 1, encapsulatedRequest.length); 94 kemAlgorithmSpec = getKem(encapsulatedRequest); 95 kdfAlgorithmSpec = getKdf(encapsulatedRequest); 96 aeadAlgorithmSpec = getAead(encapsulatedRequest); 97 } 98 int keyId = getKeyId(encapsulatedRequest); 99 100 EncapsulatedSharedSecret encapsulatedSharedSecret = 101 getEncapsulatedSharedSecret( 102 kemAlgorithmSpec.encapsulatedKeyLength(), encapsulatedRequest); 103 104 // Compute the recipient info required to decrypt the payload 105 // As per https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-10 106 RecipientKeyInfo info = 107 createRecipientKeyInfo( 108 keyId, 109 kemAlgorithmSpec.identifier(), 110 kdfAlgorithmSpec.identifier(), 111 aeadAlgorithmSpec.identifier(), 112 isMediaTypeChanged); 113 114 byte[] cipherText = 115 getCipherText(kemAlgorithmSpec.encapsulatedKeyLength(), encapsulatedRequest); 116 117 OhttpJniWrapper jniWrapper = OhttpJniWrapper.getInstance(); 118 HpkeContextNativeRef hpkeContextNativeRef = 119 HpkeContextNativeRef.createHpkeContextReference(); 120 121 if (!jniWrapper.hpkeSetupRecipient( 122 hpkeContextNativeRef, 123 kemAlgorithmSpec.kemNativeRefSupplier().get(), 124 kdfAlgorithmSpec.kdfNativeRefSupplier().get(), 125 aeadAlgorithmSpec.aeadNativeRefSupplier().get(), 126 privateKey, 127 encapsulatedSharedSecret, 128 info)) { 129 return new byte[0]; 130 } 131 132 GatewayDecryptResponse decrypted = 133 jniWrapper.gatewayDecrypt( 134 hpkeContextNativeRef, 135 kemAlgorithmSpec.kemNativeRefSupplier().get(), 136 kdfAlgorithmSpec.kdfNativeRefSupplier().get(), 137 aeadAlgorithmSpec.aeadNativeRefSupplier().get(), 138 cipherText); 139 140 return decrypted.getBytes(); 141 } 142 143 /** 144 * Encrypts the given plaintext 145 * 146 * <p>The gateway/server derives the necessary materials for encryption including the key from 147 * an encrypted payload that was generated by the client. Hence, we need to provide a seed 148 * encrypted request. 149 * 150 * <p>This method will derive the necessary information from encryptedSeedRequest and encrypt 151 * the plainText 152 * 153 * <p>Encrypts payload that can be decrypted by {@link 154 * ObliviousHttpClient#decryptObliviousHttpResponse(byte[], ObliviousHttpRequestContext)} 155 * 156 * @param privateKey The private key of the server 157 * @param encryptedSeedRequest The encrypted payload from which to derive keying materials 158 * @param plainText The payload to encrypt. 159 * @return the encrypted bytes 160 */ encrypt( OhttpGatewayPrivateKey privateKey, @NonNull byte[] encryptedSeedRequest, @NonNull byte[] plainText)161 public static byte[] encrypt( 162 OhttpGatewayPrivateKey privateKey, 163 @NonNull byte[] encryptedSeedRequest, 164 @NonNull byte[] plainText) 165 throws UnsupportedHpkeAlgorithmException, IOException { 166 boolean isMediaTypeChanged = false; 167 KemAlgorithmSpec kemAlgorithmSpec; 168 KdfAlgorithmSpec kdfAlgorithmSpec; 169 AeadAlgorithmSpec aeadAlgorithmSpec; 170 171 // Parse the encapsulated request into its components 172 try { 173 kemAlgorithmSpec = getKem(encryptedSeedRequest); 174 kdfAlgorithmSpec = getKdf(encryptedSeedRequest); 175 aeadAlgorithmSpec = getAead(encryptedSeedRequest); 176 } catch (UnsupportedHpkeAlgorithmException e) { 177 // Remove version byte in case is using message/auction format (see 178 // go/fledge-ohttp-media-type-update) 179 isMediaTypeChanged = true; 180 encryptedSeedRequest = 181 Arrays.copyOfRange(encryptedSeedRequest, 1, encryptedSeedRequest.length); 182 kemAlgorithmSpec = getKem(encryptedSeedRequest); 183 kdfAlgorithmSpec = getKdf(encryptedSeedRequest); 184 aeadAlgorithmSpec = getAead(encryptedSeedRequest); 185 } 186 int keyId = getKeyId(encryptedSeedRequest); 187 188 EncapsulatedSharedSecret encapsulatedSharedSecret = 189 getEncapsulatedSharedSecret( 190 kemAlgorithmSpec.encapsulatedKeyLength(), encryptedSeedRequest); 191 192 // Compute the recipient info 193 // As per https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-10 194 RecipientKeyInfo info = 195 createRecipientKeyInfo( 196 keyId, 197 kemAlgorithmSpec.identifier(), 198 kdfAlgorithmSpec.identifier(), 199 aeadAlgorithmSpec.identifier(), 200 isMediaTypeChanged); 201 202 /** 203 * The encryption algorithm is as follows 204 * https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-3 205 * 206 * <pre> secret = context.Export("message/label response", Nk) 207 * response_nonce = random(max(Nn, Nk)) 208 * salt = concat(enc, response_nonce) 209 * prk = Extract(salt, secret) 210 * aead_key = Expand(prk, "key", Nk) 211 * aead_nonce = Expand(prk, "nonce", Nn) 212 * ct = Seal(aead_key, aead_nonce, "", response) 213 * enc_response = concat(response_nonce, ct) 214 * </pre> 215 */ 216 217 // Create and set up the recipient context 218 OhttpJniWrapper jniWrapper = OhttpJniWrapper.getInstance(); 219 HpkeContextNativeRef hpkeContextNativeRef = 220 HpkeContextNativeRef.createHpkeContextReference(); 221 222 if (!jniWrapper.hpkeSetupRecipient( 223 hpkeContextNativeRef, 224 kemAlgorithmSpec.kemNativeRefSupplier().get(), 225 kdfAlgorithmSpec.kdfNativeRefSupplier().get(), 226 aeadAlgorithmSpec.aeadNativeRefSupplier().get(), 227 privateKey, 228 encapsulatedSharedSecret, 229 info)) { 230 return new byte[0]; 231 } 232 233 // secret = context.Export("message/label response", Nk) 234 byte[] labelBytes = 235 ObliviousHttpKeyConfig.getOhttpResponseLabel(isMediaTypeChanged) 236 .getBytes(StandardCharsets.US_ASCII); 237 HpkeExportResponse secret = 238 jniWrapper.hpkeExport( 239 hpkeContextNativeRef, labelBytes, aeadAlgorithmSpec.keyLength()); 240 241 // response_nonce = random(max(Nn, Nk)) 242 int lengthNonce = Math.max(aeadAlgorithmSpec.nonceLength(), aeadAlgorithmSpec.keyLength()); 243 byte[] responseNonce = getSecureRandomBytes(lengthNonce); 244 245 // salt = concat(enc, response_nonce) 246 byte[] salt = concatByteArrays(encapsulatedSharedSecret.getBytes(), responseNonce); 247 248 HkdfMessageDigestNativeRef messageDigest = kdfAlgorithmSpec.messageDigestSupplier().get(); 249 // prk = Extract(salt, secret) 250 byte[] prk = extract(jniWrapper, messageDigest, secret.getBytes(), salt); 251 252 // aead_key = Expand(prk, "key", Nk) 253 byte[] keyContext = AEAD_KEY_CONTEXT.getBytes(StandardCharsets.US_ASCII); 254 HkdfExpandResponse hkdfKeyResponse = 255 jniWrapper.hkdfExpand( 256 messageDigest, prk, keyContext, aeadAlgorithmSpec.keyLength()); 257 258 // aead_nonce = Expand(prk, "nonce", Nn) 259 byte[] nonceContext = AEAD_NONCE_CONTEXT.getBytes(StandardCharsets.US_ASCII); 260 HkdfExpandResponse hkdfNonceResponse = 261 jniWrapper.hkdfExpand( 262 messageDigest, prk, nonceContext, aeadAlgorithmSpec.nonceLength()); 263 264 // ct = Seal(aead_key, aead_nonce, "", response) 265 byte[] cipherText = 266 jniWrapper.aeadSeal( 267 aeadAlgorithmSpec.aeadNativeRefSupplier().get(), 268 hkdfKeyResponse.getBytes(), 269 hkdfNonceResponse.getBytes(), 270 plainText); 271 272 // enc_response = concat(response_nonce, ct) 273 return concatByteArrays(responseNonce, cipherText); 274 } 275 extract( OhttpJniWrapper ohttpJniWrapper, HkdfMessageDigestNativeRef messageDigest, byte[] secret, byte[] salt)276 private static byte[] extract( 277 OhttpJniWrapper ohttpJniWrapper, 278 HkdfMessageDigestNativeRef messageDigest, 279 byte[] secret, 280 byte[] salt) { 281 HkdfExtractResponse extractResponse = 282 ohttpJniWrapper.hkdfExtract(messageDigest, secret, salt); 283 return extractResponse.getBytes(); 284 } 285 concatByteArrays(byte[] array1, byte[] array2)286 private static byte[] concatByteArrays(byte[] array1, byte[] array2) throws IOException { 287 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 288 outputStream.write(array1); 289 outputStream.write(array2); 290 return outputStream.toByteArray(); 291 } 292 getKeyId(byte[] cipherText)293 private static int getKeyId(byte[] cipherText) { 294 return cipherText[0]; 295 } 296 getKem(byte[] cipherText)297 private static KemAlgorithmSpec getKem(byte[] cipherText) 298 throws UnsupportedHpkeAlgorithmException { 299 int kemId = ((cipherText[1] & 0xff) << 8) | (cipherText[2] & 0xff); 300 return KemAlgorithmSpec.get(kemId); 301 } 302 getKdf(byte[] cipherText)303 private static KdfAlgorithmSpec getKdf(byte[] cipherText) 304 throws UnsupportedHpkeAlgorithmException { 305 int kdfId = ((cipherText[3] & 0xff) << 8) | (cipherText[4] & 0xff); 306 return KdfAlgorithmSpec.get(kdfId); 307 } 308 getAead(byte[] cipherText)309 private static AeadAlgorithmSpec getAead(byte[] cipherText) 310 throws UnsupportedHpkeAlgorithmException { 311 int aeadId = ((cipherText[5] & 0xff) << 8) | (cipherText[6] & 0xff); 312 return AeadAlgorithmSpec.get(aeadId); 313 } 314 getEncapsulatedSharedSecret( int encLength, byte[] cipherText)315 private static EncapsulatedSharedSecret getEncapsulatedSharedSecret( 316 int encLength, byte[] cipherText) { 317 Preconditions.checkArgument( 318 encLength + MESSAGE_HEADER_LENGTH_IN_BYTES <= cipherText.length); 319 byte[] destinationArray = new byte[encLength]; 320 System.arraycopy( 321 cipherText, 322 MESSAGE_HEADER_LENGTH_IN_BYTES, 323 destinationArray, 324 /* destPos= */ 0, 325 encLength); 326 return EncapsulatedSharedSecret.create(destinationArray); 327 } 328 329 /** 330 * Generates the 'info' field as required by HPKE setupBaseR operation according to OHTTP spec 331 * 332 * <p>https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-10 333 * 334 * <pre>info = concat(encode_str("message/label request"), 335 * encode(1, 0), 336 * encode(1, keyID), 337 * encode(2, kemID), 338 * encode(2, kdfID), 339 * encode(2, aeadID)) </pre> 340 */ createRecipientKeyInfo( int keyId, int kemId, int kdfId, int aeadId, boolean isMediaTypeChanged)341 private static RecipientKeyInfo createRecipientKeyInfo( 342 int keyId, int kemId, int kdfId, int aeadId, boolean isMediaTypeChanged) 343 throws IOException { 344 try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 345 DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream)) { 346 byte[] ohttpReqLabelBytes = 347 ObliviousHttpKeyConfig.getOhttpRequestLabel(isMediaTypeChanged) 348 .getBytes(StandardCharsets.US_ASCII); 349 dataOutputStream.write(ohttpReqLabelBytes); 350 dataOutputStream.writeByte(0); 351 352 // TODO(b/309095970) : Extract OhttpMessageHeader into its own class 353 dataOutputStream.writeByte(keyId); 354 dataOutputStream.writeShort(kemId); 355 dataOutputStream.writeShort(kdfId); 356 dataOutputStream.writeShort(aeadId); 357 dataOutputStream.flush(); 358 359 return RecipientKeyInfo.create(byteArrayOutputStream.toByteArray()); 360 } 361 } 362 getCipherText(int encLength, byte[] encapsulatedRequest)363 private static byte[] getCipherText(int encLength, byte[] encapsulatedRequest) { 364 Preconditions.checkArgument( 365 encLength + MESSAGE_HEADER_LENGTH_IN_BYTES <= encapsulatedRequest.length); 366 int sizeOfCipherText = 367 encapsulatedRequest.length - (MESSAGE_HEADER_LENGTH_IN_BYTES + encLength); 368 byte[] destinationArray = new byte[sizeOfCipherText]; 369 System.arraycopy( 370 encapsulatedRequest, 371 /* srcPos= */ encLength + MESSAGE_HEADER_LENGTH_IN_BYTES, 372 destinationArray, 373 /* destPos= */ 0, 374 sizeOfCipherText); 375 return destinationArray; 376 } 377 getSecureRandomBytes(int length)378 private static byte[] getSecureRandomBytes(int length) { 379 SecureRandom random = new SecureRandom(); 380 byte[] token = new byte[length]; 381 random.nextBytes(token); 382 383 return token; 384 } 385 } 386