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 com.android.adservices.ohttp.algorithms.AeadAlgorithmSpec; 20 import com.android.adservices.ohttp.algorithms.HpkeAlgorithmSpec; 21 import com.android.adservices.ohttp.algorithms.UnsupportedHpkeAlgorithmException; 22 import com.android.internal.annotations.VisibleForTesting; 23 24 import java.io.ByteArrayOutputStream; 25 import java.io.IOException; 26 import java.nio.charset.StandardCharsets; 27 import java.security.SecureRandom; 28 import java.util.Arrays; 29 30 /** 31 * Provides methods for OHTTP client side encryption and decryption 32 * 33 * <ul> 34 * <li>Facilitates client side to initiate OHttp request flow by initializing the key config 35 * obtained from server, and subsequently uses it to encrypt the request payload. 36 * <li>After initializing this class with server's key config, users can call 37 * `CreateObliviousHttpRequest` which constructs OHTTP request of the input payload. 38 * <li>Handles decryption of response that will be sent back from Server in HTTP POST body. 39 * <li>Handles BoringSSL HPKE context setup and bookkeeping. 40 * </ul> 41 */ 42 public class ObliviousHttpClient { 43 // HPKE export methods require context strings 44 // Context strings to export aead key and nonce as defined in 45 // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-3 46 private static String sAeadKeyContext = "key"; 47 private static String sAeadNonceContext = "nonce"; 48 49 private ObliviousHttpKeyConfig mObliviousHttpKeyConfig; 50 private HpkeAlgorithmSpec mHpkeAlgorithmSpec; 51 ObliviousHttpClient(ObliviousHttpKeyConfig keyConfig, HpkeAlgorithmSpec algorithmSpec)52 private ObliviousHttpClient(ObliviousHttpKeyConfig keyConfig, HpkeAlgorithmSpec algorithmSpec) { 53 mObliviousHttpKeyConfig = keyConfig; 54 mHpkeAlgorithmSpec = algorithmSpec; 55 } 56 57 /** 58 * Creates the ObliviousHttpClient and initializes it with the given obliviousHttpKeyConfig 59 * 60 * @throws UnsupportedHpkeAlgorithmException if the key config specifies unsupported OHTTP/HPKE 61 * algorithms 62 */ create(ObliviousHttpKeyConfig keyConfig)63 public static ObliviousHttpClient create(ObliviousHttpKeyConfig keyConfig) 64 throws UnsupportedHpkeAlgorithmException { 65 HpkeAlgorithmSpec hpkeAlgorithmSpec = HpkeAlgorithmSpec.fromKeyConfig(keyConfig); 66 return new ObliviousHttpClient(keyConfig, hpkeAlgorithmSpec); 67 } 68 69 /** 70 * Takes the plainText byte array and returns an ObliviousHttpRequest object which contains the 71 * shared secret and the cipher text along with HPKE context. 72 */ createObliviousHttpRequest( byte[] plainText, boolean hasMediaTypeChanged)73 public ObliviousHttpRequest createObliviousHttpRequest( 74 byte[] plainText, boolean hasMediaTypeChanged) throws IOException { 75 byte[] seed = getSecureRandomBytes(mHpkeAlgorithmSpec.kem().seedLength()); 76 return createObliviousHttpRequest(plainText, seed, hasMediaTypeChanged); 77 } 78 79 /** 80 * Creates an Oblivious Http Request with the given seed to produce deterministic shared secret 81 * 82 * <p>Should only be used for testing purposes. For production uses, seeds should be randomly 83 * generated and cryptographically safe. 84 */ 85 @VisibleForTesting createObliviousHttpRequest( byte[] plainText, byte[] seed, boolean hasMediaTypeChanged)86 public ObliviousHttpRequest createObliviousHttpRequest( 87 byte[] plainText, byte[] seed, boolean hasMediaTypeChanged) throws IOException { 88 HpkeContextNativeRef hpkeContextNativeRef = 89 HpkeContextNativeRef.createHpkeContextReference(); 90 KemNativeRef kemNativeRef = mHpkeAlgorithmSpec.kem().kemNativeRefSupplier().get(); 91 KdfNativeRef kdfAlgorithmSpec = mHpkeAlgorithmSpec.kdf().kdfNativeRefSupplier().get(); 92 AeadNativeRef aeadNativeRef = mHpkeAlgorithmSpec.aead().aeadNativeRefSupplier().get(); 93 94 RecipientKeyInfo recipientKeyInfo = 95 mObliviousHttpKeyConfig.createRecipientKeyInfo(hasMediaTypeChanged); 96 OhttpJniWrapper ohttpJniWrapper = OhttpJniWrapper.getInstance(); 97 HpkeEncryptResponse encryptResponse = 98 ohttpJniWrapper.hpkeEncrypt( 99 hpkeContextNativeRef, 100 kemNativeRef, 101 kdfAlgorithmSpec, 102 aeadNativeRef, 103 mObliviousHttpKeyConfig.publicKey(), 104 recipientKeyInfo.getBytes(), 105 seed, 106 plainText, 107 /* aad= */ null); 108 109 ObliviousHttpRequestContext requestContext = 110 ObliviousHttpRequestContext.create( 111 mObliviousHttpKeyConfig, 112 encryptResponse.encapsulatedSharedSecret(), 113 seed, 114 hasMediaTypeChanged); 115 return ObliviousHttpRequest.create(plainText, encryptResponse.cipherText(), requestContext); 116 } 117 118 /** 119 * Takes the ciphertext returned by the server and decrypts it 120 * 121 * <p>Decrypt as per https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-3 122 * 123 * @param encryptedResponse The encrypted response to be decrypted 124 * @param requestContext The ObliviousHttpRequestContext generated during call to 125 * createObliviousHttpRequest 126 * @return the decrypted response 127 */ decryptObliviousHttpResponse( byte[] encryptedResponse, ObliviousHttpRequestContext requestContext)128 public byte[] decryptObliviousHttpResponse( 129 byte[] encryptedResponse, ObliviousHttpRequestContext requestContext) 130 throws IOException { 131 OhttpJniWrapper ohttpJniWrapper = OhttpJniWrapper.getInstance(); 132 133 // secret = context.Export("message/label response", Nk) 134 byte[] secret = export(ohttpJniWrapper, requestContext); 135 136 byte[] responseNonce = extractResponseNonce(encryptedResponse); 137 138 // salt = concat(enc, response_nonce) 139 byte[] salt = concatSalt(requestContext, responseNonce); 140 141 HkdfMessageDigestNativeRef messageDigest = 142 mHpkeAlgorithmSpec.kdf().messageDigestSupplier().get(); 143 144 // prk = Extract(salt, secret) 145 byte[] prk = extract(ohttpJniWrapper, messageDigest, secret, salt); 146 147 // aead_key = Expand(prk, "key", Nk) 148 byte[] keyContext = sAeadKeyContext.getBytes(StandardCharsets.US_ASCII); 149 AeadAlgorithmSpec aead = mHpkeAlgorithmSpec.aead(); 150 HkdfExpandResponse hkdfKeyResponse = 151 ohttpJniWrapper.hkdfExpand(messageDigest, prk, keyContext, aead.keyLength()); 152 153 // aead_nonce = Expand(prk, "nonce", Nn) 154 byte[] nonceContext = sAeadNonceContext.getBytes(StandardCharsets.US_ASCII); 155 HkdfExpandResponse hkdfNonceResponse = 156 ohttpJniWrapper.hkdfExpand(messageDigest, prk, nonceContext, aead.nonceLength()); 157 158 AeadNativeRef aeadNativeRef = aead.aeadNativeRefSupplier().get(); 159 byte[] cipherText = extractCipherText(encryptedResponse); 160 byte[] decrypted = 161 ohttpJniWrapper.aeadOpen( 162 aeadNativeRef, 163 hkdfKeyResponse.getBytes(), 164 hkdfNonceResponse.getBytes(), 165 cipherText); 166 167 return decrypted; 168 } 169 170 @VisibleForTesting getHpkeAlgorithmSpec()171 HpkeAlgorithmSpec getHpkeAlgorithmSpec() { 172 return mHpkeAlgorithmSpec; 173 } 174 extractResponseNonce(byte[] encryptedResponse)175 private byte[] extractResponseNonce(byte[] encryptedResponse) { 176 // enc_response = concat(response_nonce, ct) 177 // Length of response nonce => max(Nn, Nk) 178 179 AeadAlgorithmSpec aead = mHpkeAlgorithmSpec.aead(); 180 int lengthNonce = Math.max(aead.nonceLength(), aead.keyLength()); 181 182 return Arrays.copyOfRange(encryptedResponse, 0, lengthNonce); 183 } 184 extractCipherText(byte[] encryptedResponse)185 private byte[] extractCipherText(byte[] encryptedResponse) { 186 // enc_response = concat(response_nonce, ct) 187 // Length of response nonce => max(Nn, Nk) 188 189 AeadAlgorithmSpec aead = mHpkeAlgorithmSpec.aead(); 190 int lengthNonce = Math.max(aead.nonceLength(), aead.keyLength()); 191 192 return Arrays.copyOfRange(encryptedResponse, lengthNonce, encryptedResponse.length); 193 } 194 getSecureRandomBytes(int length)195 private byte[] getSecureRandomBytes(int length) { 196 SecureRandom random = new SecureRandom(); 197 byte[] token = new byte[length]; 198 random.nextBytes(token); 199 200 return token; 201 } 202 export( OhttpJniWrapper ohttpJniWrapper, ObliviousHttpRequestContext requestContext)203 private byte[] export( 204 OhttpJniWrapper ohttpJniWrapper, ObliviousHttpRequestContext requestContext) 205 throws IOException { 206 byte[] labelBytes = 207 ObliviousHttpKeyConfig.getOhttpResponseLabel(requestContext.hasMediaTypeChanged()) 208 .getBytes(StandardCharsets.US_ASCII); 209 210 // Regenerate HPKE context 211 KemNativeRef kemNativeRef = mHpkeAlgorithmSpec.kem().kemNativeRefSupplier().get(); 212 KdfNativeRef kdfAlgorithmSpec = mHpkeAlgorithmSpec.kdf().kdfNativeRefSupplier().get(); 213 AeadNativeRef aeadNativeRef = mHpkeAlgorithmSpec.aead().aeadNativeRefSupplier().get(); 214 RecipientKeyInfo recipientKeyInfo = 215 mObliviousHttpKeyConfig.createRecipientKeyInfo( 216 requestContext.hasMediaTypeChanged()); 217 HpkeContextNativeRef hpkectx = HpkeContextNativeRef.createHpkeContextReference(); 218 ohttpJniWrapper.hpkeCtxSetupSenderWithSeed( 219 hpkectx, 220 kemNativeRef, 221 kdfAlgorithmSpec, 222 aeadNativeRef, 223 mObliviousHttpKeyConfig.publicKey(), 224 recipientKeyInfo.getBytes(), 225 requestContext.seed()); 226 227 HpkeExportResponse exportResponse = 228 ohttpJniWrapper.hpkeExport( 229 hpkectx, labelBytes, mHpkeAlgorithmSpec.aead().keyLength()); 230 return exportResponse.getBytes(); 231 } 232 concatSalt(ObliviousHttpRequestContext requestContext, byte[] responseNonce)233 private byte[] concatSalt(ObliviousHttpRequestContext requestContext, byte[] responseNonce) 234 throws IOException { 235 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 236 outputStream.write(requestContext.encapsulatedSharedSecret().getBytes()); 237 outputStream.write(responseNonce); 238 return outputStream.toByteArray(); 239 } 240 extract( OhttpJniWrapper ohttpJniWrapper, HkdfMessageDigestNativeRef messageDigest, byte[] secret, byte[] salt)241 private byte[] extract( 242 OhttpJniWrapper ohttpJniWrapper, 243 HkdfMessageDigestNativeRef messageDigest, 244 byte[] secret, 245 byte[] salt) { 246 HkdfExtractResponse extractResponse = 247 ohttpJniWrapper.hkdfExtract(messageDigest, secret, salt); 248 return extractResponse.getBytes(); 249 } 250 } 251