1 /* 2 * Copyright (C) 2020 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.car.encryptionrunner; 18 19 import android.util.Log; 20 21 import androidx.annotation.IntDef; 22 import androidx.annotation.NonNull; 23 import androidx.annotation.Nullable; 24 import androidx.annotation.VisibleForTesting; 25 26 import com.google.security.cryptauth.lib.securegcm.D2DConnectionContext; 27 import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake; 28 import com.google.security.cryptauth.lib.securemessage.CryptoOps; 29 30 import java.io.ByteArrayOutputStream; 31 import java.io.IOException; 32 import java.security.InvalidKeyException; 33 import java.security.MessageDigest; 34 import java.security.NoSuchAlgorithmException; 35 import java.security.SignatureException; 36 37 import javax.crypto.spec.SecretKeySpec; 38 39 /** 40 * An {@link EncryptionRunner} that uses Ukey2 as the underlying implementation. 41 */ 42 public class Ukey2EncryptionRunner implements EncryptionRunner { 43 44 private static final Ukey2Handshake.HandshakeCipher CIPHER = 45 Ukey2Handshake.HandshakeCipher.P256_SHA512; 46 private static final int RESUME_HMAC_LENGTH = 32; 47 private static final byte[] RESUME = "RESUME".getBytes(); 48 private static final byte[] SERVER = "SERVER".getBytes(); 49 private static final byte[] CLIENT = "CLIENT".getBytes(); 50 private static final int AUTH_STRING_LENGTH = 6; 51 52 @IntDef({Mode.UNKNOWN, Mode.CLIENT, Mode.SERVER}) 53 private @interface Mode { 54 int UNKNOWN = 0; 55 int CLIENT = 1; 56 int SERVER = 2; 57 } 58 59 private Ukey2Handshake mUkey2client; 60 private boolean mRunnerIsInvalid; 61 private Key mCurrentKey; 62 private byte[] mCurrentUniqueSesion; 63 private byte[] mPrevUniqueSesion; 64 private boolean mIsReconnect; 65 private boolean mInitReconnectionVerification; 66 @Mode 67 private int mMode = Mode.UNKNOWN; 68 69 @Override initHandshake()70 public HandshakeMessage initHandshake() { 71 checkRunnerIsNew(); 72 mMode = Mode.CLIENT; 73 try { 74 mUkey2client = Ukey2Handshake.forInitiator(CIPHER); 75 return HandshakeMessage.newBuilder() 76 .setHandshakeState(getHandshakeState()) 77 .setNextMessage(mUkey2client.getNextHandshakeMessage()) 78 .build(); 79 } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException e) { 80 Log.e(TAG, "unexpected exception", e); 81 throw new RuntimeException(e); 82 } 83 84 } 85 86 @Override setIsReconnect(boolean isReconnect)87 public void setIsReconnect(boolean isReconnect) { 88 mIsReconnect = isReconnect; 89 } 90 91 @Override respondToInitRequest(byte[] initializationRequest)92 public HandshakeMessage respondToInitRequest(byte[] initializationRequest) 93 throws HandshakeException { 94 checkRunnerIsNew(); 95 mMode = Mode.SERVER; 96 try { 97 if (mUkey2client != null) { 98 throw new IllegalStateException("Cannot reuse encryption runners, " 99 + "this one is already initialized"); 100 } 101 mUkey2client = Ukey2Handshake.forResponder(CIPHER); 102 mUkey2client.parseHandshakeMessage(initializationRequest); 103 return HandshakeMessage.newBuilder() 104 .setHandshakeState(getHandshakeState()) 105 .setNextMessage(mUkey2client.getNextHandshakeMessage()) 106 .build(); 107 108 } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException 109 | Ukey2Handshake.AlertException e) { 110 throw new HandshakeException(e); 111 } 112 } 113 checkRunnerIsNew()114 private void checkRunnerIsNew() { 115 if (mUkey2client != null) { 116 throw new IllegalStateException("This runner is already initialized."); 117 } 118 } 119 120 121 @Override continueHandshake(byte[] response)122 public HandshakeMessage continueHandshake(byte[] response) throws HandshakeException { 123 checkInitialized(); 124 try { 125 if (mUkey2client.getHandshakeState() != Ukey2Handshake.State.IN_PROGRESS) { 126 throw new IllegalStateException("handshake is not in progress, state =" 127 + mUkey2client.getHandshakeState()); 128 } 129 mUkey2client.parseHandshakeMessage(response); 130 131 // Not obvious from ukey2 api, but getting the next message can change the state. 132 // calling getNext message might go from in progress to verification needed, on 133 // the assumption that we already send this message to the peer. 134 byte[] nextMessage = null; 135 if (mUkey2client.getHandshakeState() == Ukey2Handshake.State.IN_PROGRESS) { 136 nextMessage = mUkey2client.getNextHandshakeMessage(); 137 } 138 String verificationCode = null; 139 if (mUkey2client.getHandshakeState() == Ukey2Handshake.State.VERIFICATION_NEEDED) { 140 // getVerificationString() needs to be called before verifyPin(). 141 verificationCode = generateReadablePairingCode( 142 mUkey2client.getVerificationString(AUTH_STRING_LENGTH)); 143 if (mIsReconnect) { 144 HandshakeMessage handshakeMessage = verifyPin(); 145 return HandshakeMessage.newBuilder() 146 .setHandshakeState(handshakeMessage.getHandshakeState()) 147 .setNextMessage(nextMessage) 148 .build(); 149 } 150 } 151 return HandshakeMessage.newBuilder() 152 .setHandshakeState(getHandshakeState()) 153 .setNextMessage(nextMessage) 154 .setVerificationCode(verificationCode) 155 .build(); 156 } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException 157 | Ukey2Handshake.AlertException e) { 158 throw new HandshakeException(e); 159 } 160 } 161 162 /** 163 * Returns a human-readable pairing code string generated from the verification bytes. Converts 164 * each byte into a digit with a simple modulo. 165 * 166 * <p>This should match the implementation in the iOS and Android client libraries. 167 */ 168 @VisibleForTesting generateReadablePairingCode(byte[] verificationCode)169 String generateReadablePairingCode(byte[] verificationCode) { 170 StringBuilder outString = new StringBuilder(); 171 for (byte b : verificationCode) { 172 int unsignedInt = Byte.toUnsignedInt(b); 173 int digit = unsignedInt % 10; 174 outString.append(digit); 175 } 176 177 return outString.toString(); 178 } 179 180 private static class UKey2Key implements Key { 181 182 private final D2DConnectionContext mConnectionContext; 183 UKey2Key(@onNull D2DConnectionContext connectionContext)184 UKey2Key(@NonNull D2DConnectionContext connectionContext) { 185 this.mConnectionContext = connectionContext; 186 } 187 188 @Override asBytes()189 public byte[] asBytes() { 190 return mConnectionContext.saveSession(); 191 } 192 193 @Override encryptData(byte[] data)194 public byte[] encryptData(byte[] data) { 195 return mConnectionContext.encodeMessageToPeer(data); 196 } 197 198 @Override decryptData(byte[] encryptedData)199 public byte[] decryptData(byte[] encryptedData) throws SignatureException { 200 return mConnectionContext.decodeMessageFromPeer(encryptedData); 201 } 202 203 @Override getUniqueSession()204 public byte[] getUniqueSession() throws NoSuchAlgorithmException { 205 return mConnectionContext.getSessionUnique(); 206 } 207 } 208 209 @Override verifyPin()210 public HandshakeMessage verifyPin() throws HandshakeException { 211 checkInitialized(); 212 mUkey2client.verifyHandshake(); 213 int state = getHandshakeState(); 214 try { 215 mCurrentKey = new UKey2Key(mUkey2client.toConnectionContext()); 216 } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException e) { 217 throw new HandshakeException(e); 218 } 219 return HandshakeMessage.newBuilder() 220 .setHandshakeState(state) 221 .setKey(mCurrentKey) 222 .build(); 223 } 224 225 /** 226 * <p>After getting message from the other device, authenticate the message with the previous 227 * stored key. 228 * 229 * If current device inits the reconnection authentication by calling {@code 230 * initReconnectAuthentication} and sends the message to the other device, the other device 231 * will call {@code authenticateReconnection()} with the received message and send its own 232 * message back to the init device. The init device will call {@code 233 * authenticateReconnection()} on the received message, but do not need to set the next 234 * message. 235 */ 236 @Override authenticateReconnection(byte[] message, byte[] previousKey)237 public HandshakeMessage authenticateReconnection(byte[] message, byte[] previousKey) 238 throws HandshakeException { 239 if (!mIsReconnect) { 240 throw new HandshakeException( 241 "Reconnection authentication requires setIsReconnect(true)"); 242 } 243 if (mCurrentKey == null) { 244 throw new HandshakeException("Current key is null, make sure verifyPin() is called."); 245 } 246 if (message.length != RESUME_HMAC_LENGTH) { 247 mRunnerIsInvalid = true; 248 throw new HandshakeException("Failing because (message.length =" + message.length 249 + ") is not equal to " + RESUME_HMAC_LENGTH); 250 } 251 try { 252 mCurrentUniqueSesion = mCurrentKey.getUniqueSession(); 253 mPrevUniqueSesion = keyOf(previousKey).getUniqueSession(); 254 } catch (NoSuchAlgorithmException e) { 255 throw new HandshakeException(e); 256 } 257 switch (mMode) { 258 case Mode.SERVER: 259 if (!MessageDigest.isEqual( 260 message, computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, CLIENT))) { 261 mRunnerIsInvalid = true; 262 throw new HandshakeException("Reconnection authentication failed."); 263 } 264 return HandshakeMessage.newBuilder() 265 .setHandshakeState(HandshakeMessage.HandshakeState.FINISHED) 266 .setKey(mCurrentKey) 267 .setNextMessage(mInitReconnectionVerification ? null 268 : computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, SERVER)) 269 .build(); 270 case Mode.CLIENT: 271 if (!MessageDigest.isEqual( 272 message, computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, SERVER))) { 273 mRunnerIsInvalid = true; 274 throw new HandshakeException("Reconnection authentication failed."); 275 } 276 return HandshakeMessage.newBuilder() 277 .setHandshakeState(HandshakeMessage.HandshakeState.FINISHED) 278 .setKey(mCurrentKey) 279 .setNextMessage(mInitReconnectionVerification ? null 280 : computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, CLIENT)) 281 .build(); 282 default: 283 throw new IllegalStateException( 284 "Encountered unexpected role during authenticateReconnection: " + mMode); 285 } 286 } 287 288 /** 289 * Both client and server can call this method to send authentication message to the other 290 * device. 291 */ 292 @Override initReconnectAuthentication(byte[] previousKey)293 public HandshakeMessage initReconnectAuthentication(byte[] previousKey) 294 throws HandshakeException { 295 if (!mIsReconnect) { 296 throw new HandshakeException( 297 "Reconnection authentication requires setIsReconnect(true)."); 298 } 299 if (mCurrentKey == null) { 300 throw new HandshakeException("Current key is null, make sure verifyPin() is called."); 301 } 302 mInitReconnectionVerification = true; 303 try { 304 mCurrentUniqueSesion = mCurrentKey.getUniqueSession(); 305 mPrevUniqueSesion = keyOf(previousKey).getUniqueSession(); 306 } catch (NoSuchAlgorithmException e) { 307 throw new HandshakeException(e); 308 } 309 switch (mMode) { 310 case Mode.SERVER: 311 return HandshakeMessage.newBuilder() 312 .setHandshakeState(HandshakeMessage.HandshakeState.RESUMING_SESSION) 313 .setNextMessage(computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, SERVER)) 314 .build(); 315 case Mode.CLIENT: 316 return HandshakeMessage.newBuilder() 317 .setHandshakeState(HandshakeMessage.HandshakeState.RESUMING_SESSION) 318 .setNextMessage(computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, CLIENT)) 319 .build(); 320 default: 321 throw new IllegalStateException( 322 "Encountered unexpected role during authenticateReconnection: " + mMode); 323 } 324 } 325 getUkey2Client()326 protected final Ukey2Handshake getUkey2Client() { 327 return mUkey2client; 328 } 329 isReconnect()330 protected final boolean isReconnect() { 331 return mIsReconnect; 332 } 333 334 @HandshakeMessage.HandshakeState getHandshakeState()335 private int getHandshakeState() { 336 checkInitialized(); 337 switch (mUkey2client.getHandshakeState()) { 338 case ALREADY_USED: 339 case ERROR: 340 throw new IllegalStateException("unexpected error state"); 341 case FINISHED: 342 if (mIsReconnect) { 343 return HandshakeMessage.HandshakeState.RESUMING_SESSION; 344 } 345 return HandshakeMessage.HandshakeState.FINISHED; 346 case IN_PROGRESS: 347 return HandshakeMessage.HandshakeState.IN_PROGRESS; 348 case VERIFICATION_IN_PROGRESS: 349 case VERIFICATION_NEEDED: 350 return HandshakeMessage.HandshakeState.VERIFICATION_NEEDED; 351 default: 352 throw new IllegalStateException("unexpected handshake state"); 353 } 354 } 355 356 @Override keyOf(byte[] serialized)357 public Key keyOf(byte[] serialized) { 358 return new UKey2Key(D2DConnectionContext.fromSavedSession(serialized)); 359 } 360 361 @Override invalidPin()362 public void invalidPin() { 363 mRunnerIsInvalid = true; 364 } 365 checkIsUkey2Key(Key key)366 private UKey2Key checkIsUkey2Key(Key key) { 367 if (!(key instanceof UKey2Key)) { 368 throw new IllegalArgumentException("wrong key type"); 369 } 370 return (UKey2Key) key; 371 } 372 checkInitialized()373 protected void checkInitialized() { 374 if (mUkey2client == null) { 375 throw new IllegalStateException("runner not initialized"); 376 } 377 if (mRunnerIsInvalid) { 378 throw new IllegalStateException("runner has been invalidated"); 379 } 380 } 381 382 @Nullable computeMAC(byte[] previous, byte[] next, byte[] info)383 private byte[] computeMAC(byte[] previous, byte[] next, byte[] info) { 384 try { 385 SecretKeySpec inputKeyMaterial = new SecretKeySpec( 386 concatByteArrays(previous, next), "" /* key type is just plain raw bytes */); 387 return CryptoOps.hkdf(inputKeyMaterial, RESUME, info); 388 } catch (NoSuchAlgorithmException | InvalidKeyException e) { 389 // Does not happen in practice 390 Log.e(TAG, "Compute MAC failed"); 391 return null; 392 } 393 } 394 concatByteArrays(@onNull byte[] a, @NonNull byte[] b)395 private static byte[] concatByteArrays(@NonNull byte[] a, @NonNull byte[] b) { 396 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 397 try { 398 outputStream.write(a); 399 outputStream.write(b); 400 } catch (IOException e) { 401 return new byte[0]; 402 } 403 return outputStream.toByteArray(); 404 } 405 } 406