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 17 package android.crypto.hpke; 18 19 import android.annotation.FlaggedApi; 20 21 import libcore.util.NonNull; 22 import libcore.util.Nullable; 23 24 import java.security.GeneralSecurityException; 25 import java.security.InvalidKeyException; 26 import java.security.NoSuchAlgorithmException; 27 import java.security.NoSuchProviderException; 28 import java.security.PrivateKey; 29 import java.security.Provider; 30 import java.security.PublicKey; 31 import java.security.Security; 32 33 /** 34 * Provides access to implementations of HPKE hybrid cryptography as per RFC 9180. 35 * <p> 36 * Provider and HPKE suite selection are done via the {@code getInstance} 37 * methods, and then instances of senders and receivers can be created using 38 * {@code newSender} or {newReceiver}. Each sender and receiver is independent, i.e. does 39 * not share any encapsulated state with other senders or receivers created via this 40 * {@code Hpke}. 41 * <p> 42 * HPKE suites are composed of a key encapsulation mechanism (KEM), a key derivation 43 * function (KDF) and an authenticated cipher algorithm (AEAD) as defined in 44 * RFC 9180 section 7. {@link java.security.spec.NamedParameterSpec NamedParameterSpecs} for 45 * these can be found in {@link KemParameterSpec}, {@link KdfParameterSpec} and 46 * {@link AeadParameterSpec}. These can be composed into a full HPKE suite name used to 47 * request a particular implementation using 48 * {@link Hpke#getSuiteName(KemParameterSpec, KdfParameterSpec, AeadParameterSpec)}. 49 * 50 * @see KemParameterSpec 51 * @see KdfParameterSpec 52 * @see AeadParameterSpec 53 */ 54 @SuppressWarnings("NewApi") // Public HPKE classes are always all present together. 55 @FlaggedApi(com.android.libcore.Flags.FLAG_HPKE_PUBLIC_API) 56 public class Hpke { 57 private static final String SERVICE_NAME = "ConscryptHpke"; 58 static final byte[] DEFAULT_PSK = new byte[0]; 59 static final byte[] DEFAULT_PSK_ID = DEFAULT_PSK; 60 private final Provider provider; 61 private final Provider.Service service; 62 Hpke(@onNull String suiteName, @NonNull Provider provider)63 private Hpke(@NonNull String suiteName, @NonNull Provider provider) 64 throws NoSuchAlgorithmException { 65 this.provider = provider; 66 service = getService(provider, suiteName); 67 if (service == null) { 68 throw new NoSuchAlgorithmException("No such HPKE suite: " + suiteName); 69 } 70 } 71 findFirstProvider(@onNull String suiteName)72 private static @NonNull Provider findFirstProvider(@NonNull String suiteName) 73 throws NoSuchAlgorithmException { 74 for (Provider provider : Security.getProviders()) { 75 if (getService(provider, suiteName) != null) { 76 return provider; 77 } 78 } 79 throw new NoSuchAlgorithmException("No Provider found for HPKE suite: " + suiteName); 80 } 81 82 @SuppressWarnings("InlinedApi") // For SERVICE_NAME field which belongs to this class getService(Provider provider, String suiteName)83 private static Provider.Service getService(Provider provider, String suiteName) 84 throws NoSuchAlgorithmException { 85 if (suiteName == null || suiteName.isEmpty()) { 86 throw new NoSuchAlgorithmException(); 87 } 88 return provider.getService(SERVICE_NAME, suiteName); 89 } 90 findSpi()91 @NonNull HpkeSpi findSpi() { 92 Object instance; 93 try { 94 instance = service.newInstance(null); 95 } catch (NoSuchAlgorithmException e) { 96 throw new IllegalStateException("Initialisation error", e); 97 } 98 if (instance instanceof HpkeSpi) { 99 return (HpkeSpi) instance; 100 } else { 101 DuckTypedHpkeSpi spi = DuckTypedHpkeSpi.newInstance(instance); 102 if (spi != null) { 103 return spi; 104 } 105 } 106 throw new IllegalStateException( 107 String.format("Provider %s is incorrectly configured", provider.getName())); 108 } 109 110 /** 111 * Returns the {@link Provider} being used by this Hpke instance. 112 * <p> 113 * 114 * @return the Provider 115 */ getProvider()116 public @NonNull Provider getProvider() { 117 return provider; 118 } 119 120 /** 121 * Returns an Hpke instance configured for the requested HPKE suite, using the 122 * highest priority {@link Provider} which implements it. 123 * <p> 124 * Use {@link Hpke#getSuiteName(KemParameterSpec, KdfParameterSpec, AeadParameterSpec)} for 125 * generating HPKE suite names from {@link java.security.spec.NamedParameterSpec 126 * NamedParameterSpecs} 127 * 128 * @param suiteName the HPKE suite to use 129 * @return an Hpke instance configured for the requested suite 130 * @throws NoSuchAlgorithmException if no Providers can be found for the requested suite 131 */ getInstance(@onNull String suiteName)132 public static @NonNull Hpke getInstance(@NonNull String suiteName) 133 throws NoSuchAlgorithmException { 134 return new Hpke(suiteName, findFirstProvider(suiteName)); 135 } 136 137 /** 138 * Returns an Hpke instance configured for the requested HPKE suite, using the 139 * requested {@link Provider} by name. 140 * 141 * @param suiteName the HPKE suite to use 142 * @param providerName the name of the provider to use 143 * @return an Hpke instance configured for the requested suite and Provider 144 * @throws NoSuchAlgorithmException if the named Provider does not implement this suite 145 * @throws NoSuchProviderException if no Provider with the requested name can be found 146 * @throws IllegalArgumentException if providerName is null or empty 147 */ getInstance(@onNull String suiteName, @NonNull String providerName)148 public static @NonNull Hpke getInstance(@NonNull String suiteName, @NonNull String providerName) 149 throws NoSuchAlgorithmException, NoSuchProviderException { 150 if (providerName == null || providerName.isEmpty()) { 151 throw new IllegalArgumentException("Invalid Provider Name"); 152 } 153 Provider provider = Security.getProvider(providerName); 154 if (provider == null) { 155 throw new NoSuchProviderException(); 156 } 157 return new Hpke(suiteName, provider); 158 } 159 160 /** 161 * Returns an Hpke instance configured for the requested HPKE suite, using the 162 * requested {@link Provider}. 163 * 164 * @param suiteName the HPKE suite to use 165 * @param provider the provider to use 166 * @return an Hpke instance configured for the requested suite and Provider 167 * @throws NoSuchAlgorithmException if the Provider does not implement this suite 168 * @throws IllegalArgumentException if provider is null 169 */ getInstance(@onNull String suiteName, @NonNull Provider provider)170 public static @NonNull Hpke getInstance(@NonNull String suiteName, @NonNull Provider provider) 171 throws NoSuchAlgorithmException, NoSuchProviderException { 172 if (provider == null) { 173 throw new IllegalArgumentException("Null Provider"); 174 } 175 return new Hpke(suiteName, provider); 176 } 177 178 /** 179 * Generates a full HPKE suite name from the named parameter specifications of its components, 180 * which have names reflecting their usage in RFC 9180. 181 * <p> 182 * HPKE suites are composed of a key encapsulation mechanism (KEM), a key derivation 183 * function (KDF) and an authenticated cipher algorithm (AEAD) as defined in 184 * RFC 9180 section 7. {@link java.security.spec.NamedParameterSpec NamedParameterSpecs} for 185 * these can be foundu in {@link KemParameterSpec}, {@link KdfParameterSpec} and 186 * {@link AeadParameterSpec}. 187 * 188 * @see <a href="https://www.rfc-editor.org/rfc/rfc9180.html#section-7">RFC 9180 Section 7</a> 189 * @see KemParameterSpec 190 * @see KdfParameterSpec 191 * @see AeadParameterSpec 192 * 193 * @param kem the key encapsulation mechanism to use 194 * @param kdf the key derivation function to use 195 * @param aead the AEAD cipher to use 196 * @return a fully composed HPKE suite name 197 */ getSuiteName(@onNull KemParameterSpec kem, @NonNull KdfParameterSpec kdf, @NonNull AeadParameterSpec aead)198 public static @NonNull String getSuiteName(@NonNull KemParameterSpec kem, 199 @NonNull KdfParameterSpec kdf, @NonNull AeadParameterSpec aead) { 200 return kem.getName() + "/" + kdf.getName() + "/" + aead.getName(); 201 } 202 203 /** 204 * One shot API to seal a single message using BASE mode (no authentication). 205 * 206 * @see <a href="https://www.rfc-editor.org/rfc/rfc9180.html#name-encryption-and-decryption-2"> 207 * Opening and sealing</a> 208 * @param recipientKey public key of the recipient 209 * @param info additional application-supplied information, may be null or empty 210 * @param plaintext the message to send 211 * @param aad optional additional authenticated data, may be null or empty 212 * @return a Message object containing the encapsulated key, ciphertext and aad 213 * @throws InvalidKeyException if recipientKey is null or an unsupported key format 214 */ seal(@onNull PublicKey recipientKey, @Nullable byte[] info, @NonNull byte[] plaintext, @Nullable byte[] aad)215 public @NonNull Message seal(@NonNull PublicKey recipientKey, @Nullable byte[] info, 216 @NonNull byte[] plaintext, @Nullable byte[] aad) 217 throws InvalidKeyException { 218 Sender.Builder senderBuilder = new Sender.Builder(this, recipientKey); 219 if (info != null) { 220 senderBuilder.setApplicationInfo(info); 221 } 222 Sender sender = senderBuilder.build(); 223 byte[] encapsulated = sender.getEncapsulated(); 224 byte[] ciphertext = sender.seal(plaintext, aad); 225 return new Message(encapsulated, ciphertext); 226 } 227 228 /** 229 * One shot API to open a single ciphertext using BASE mode (no authentication). 230 * 231 * @see <a href="https://www.rfc-editor.org/rfc/rfc9180.html#name-encryption-and-decryption-2"> 232 * Opening and sealing</a> 233 * @param recipientKey private key of the recipient 234 * @param info application-supplied information, may be null or empty 235 * @param message the Message to open 236 * @param aad optional additional authenticated data, may be null or empty 237 * @return decrypted plaintext 238 * @throws InvalidKeyException if recipientKey is null or an unsupported key format 239 * @throws GeneralSecurityException if the decryption fails 240 */ open( @onNull PrivateKey recipientKey, @Nullable byte[] info, @NonNull Message message, @Nullable byte[] aad)241 public @NonNull byte[] open( 242 @NonNull PrivateKey recipientKey, @Nullable byte[] info, @NonNull Message message, 243 @Nullable byte[] aad) 244 throws GeneralSecurityException, InvalidKeyException { 245 Recipient.Builder recipientBuilder 246 = new Recipient.Builder(this, message.getEncapsulated(), recipientKey); 247 if (info != null) { 248 recipientBuilder.setApplicationInfo(info); 249 } 250 return recipientBuilder.build().open(message.getCiphertext(), aad); 251 } 252 } 253