• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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