• 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 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