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.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.util.Log;
23 
24 import com.android.internal.annotations.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