• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2020 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.google.security.cryptauth.lib.securegcm;
16 
17 import com.google.common.io.BaseEncoding;
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.lang.ProcessBuilder.Redirect;
23 import java.nio.ByteBuffer;
24 import java.nio.ByteOrder;
25 import java.util.Arrays;
26 import java.util.concurrent.Callable;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.Future;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
33 import javax.annotation.Nullable;
34 
35 /**
36  * A wrapper to execute and interact with the //security/cryptauth/lib/securegcm:ukey2_shell binary.
37  *
38  * <p>This binary is a shell over the C++ implementation of the UKEY2 protocol, so this wrapper is
39  * used to test compatibility between the C++ and Java implementations.
40  *
41  * <p>The ukey2_shell is invoked as follows:
42  *
43  * <pre>{@code
44  * ukey2_shell --mode=<mode> --verification_string_length=<length>
45  * }</pre>
46  *
47  * where {@code mode={initiator, responder}} and {@code verification_string_length} is a positive
48  * integer.
49  */
50 public class Ukey2ShellCppWrapper {
51   // The path the the ukey2_shell binary.
52   private static final String BINARY_PATH = "build/src/main/cpp/src/securegcm/ukey2_shell";
53 
54   // The time to wait before timing out a read or write operation to the shell.
55   @SuppressWarnings("GoodTime") // TODO(b/147378611): store a java.time.Duration instead
56   private static final long IO_TIMEOUT_MILLIS = 5000;
57 
58   public enum Mode {
59     INITIATOR,
60     RESPONDER
61   }
62 
63   private final Mode mode;
64   private final int verificationStringLength;
65   private final ExecutorService executorService;
66 
67   @Nullable private Process shellProcess;
68   private boolean secureContextEstablished;
69 
70   /**
71    * @param mode The mode to run the shell in (initiator or responder).
72    * @param verificationStringLength The length of the verification string used in the handshake.
73    */
Ukey2ShellCppWrapper(Mode mode, int verificationStringLength)74   public Ukey2ShellCppWrapper(Mode mode, int verificationStringLength) {
75     this.mode = mode;
76     this.verificationStringLength = verificationStringLength;
77     this.executorService = Executors.newSingleThreadExecutor();
78   }
79 
80   /**
81    * Begins execution of the ukey2_shell binary.
82    *
83    * @throws IOException
84    */
startShell()85   public void startShell() throws IOException {
86     if (shellProcess != null) {
87       throw new IllegalStateException("Shell already started.");
88     }
89 
90     String modeArg = "--mode=" + getModeString();
91     String verificationStringLengthArg = "--verification_string_length=" + verificationStringLength;
92 
93     final ProcessBuilder builder =
94         new ProcessBuilder(BINARY_PATH, modeArg, verificationStringLengthArg);
95 
96     // Merge the shell's stderr with the stderr of the current process.
97     builder.redirectError(Redirect.INHERIT);
98 
99     shellProcess = builder.start();
100   }
101 
102   /**
103    * Stops execution of the ukey2_shell binary.
104    *
105    * @throws IOException
106    */
stopShell()107   public void stopShell() {
108     if (shellProcess == null) {
109       throw new IllegalStateException("Shell not started.");
110     }
111     shellProcess.destroy();
112   }
113 
114   /**
115    * @return the handshake message read from the shell.
116    * @throws IOException
117    */
readHandshakeMessage()118   public byte[] readHandshakeMessage() throws IOException {
119     return readFrameWithTimeout();
120   }
121 
122   /**
123    * Sends the handshake message to the shell.
124    *
125    * @param message
126    * @throws IOException
127    */
writeHandshakeMessage(byte[] message)128   public void writeHandshakeMessage(byte[] message) throws IOException {
129     writeFrameWithTimeout(message);
130   }
131 
132   /**
133    * Reads the auth string from the shell and compares it with {@code authString}. If verification
134    * succeeds, then write "ok" back as a confirmation.
135    *
136    * @param authString the auth string to compare to.
137    * @throws IOException
138    */
confirmAuthString(byte[] authString)139   public void confirmAuthString(byte[] authString) throws IOException {
140     byte[] shellAuthString = readFrameWithTimeout();
141     if (!Arrays.equals(authString, shellAuthString)) {
142       throw new IOException(
143           String.format(
144               "Unable to verify auth string: 0x%s != 0x%s",
145               BaseEncoding.base16().encode(authString),
146               BaseEncoding.base16().encode(shellAuthString)));
147     }
148     writeFrameWithTimeout("ok".getBytes());
149     secureContextEstablished = true;
150   }
151 
152   /**
153    * Sends {@code payload} to be encrypted by the shell. This function can only be called after a
154    * handshake is performed and a secure context established.
155    *
156    * @param payload the data to be encrypted.
157    * @return the encrypted message returned by the shell.
158    * @throws IOException
159    */
sendEncryptCommand(byte[] payload)160   public byte[] sendEncryptCommand(byte[] payload) throws IOException {
161     writeFrameWithTimeout(createExpression("encrypt", payload));
162     return readFrameWithTimeout();
163   }
164 
165   /**
166    * Sends {@code message} to be decrypted by the shell. This function can only be called after a
167    * handshake is performed and a secure context established.
168    *
169    * @param message the data to be decrypted.
170    * @return the decrypted payload returned by the shell.
171    * @throws IOException
172    */
sendDecryptCommand(byte[] message)173   public byte[] sendDecryptCommand(byte[] message) throws IOException {
174     writeFrameWithTimeout(createExpression("decrypt", message));
175     return readFrameWithTimeout();
176   }
177 
178   /**
179    * Requests the session unique value from the shell. This function can only be called after a
180    * handshake is performed and a secure context established.
181    *
182    * @return the session unique value returned by the shell.
183    * @throws IOException
184    */
sendSessionUniqueCommand()185   public byte[] sendSessionUniqueCommand() throws IOException {
186     writeFrameWithTimeout(createExpression("session_unique", null));
187     return readFrameWithTimeout();
188   }
189 
190   /**
191    * Reads a frame from the shell's stdout with a timeout.
192    *
193    * @return The contents of the frame.
194    * @throws IOException
195    */
readFrameWithTimeout()196   private byte[] readFrameWithTimeout() throws IOException {
197     Future<byte[]> future =
198         executorService.submit(
199             new Callable<byte[]>() {
200               @Override
201               public byte[] call() throws Exception {
202                 return readFrame();
203               }
204             });
205 
206     try {
207       return future.get(IO_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
208     } catch (InterruptedException | ExecutionException | TimeoutException e) {
209       throw new IOException(e);
210     }
211   }
212 
213   /**
214    * Writes a frame to the shell's stdin with a timeout.
215    *
216    * @param contents the contents of the frame.
217    * @throws IOException
218    */
writeFrameWithTimeout(final byte[] contents)219   private void writeFrameWithTimeout(final byte[] contents) throws IOException {
220     Future<?> future =
221         executorService.submit(
222             new Runnable() {
223               @Override
224               public void run() {
225                 try {
226                   writeFrame(contents);
227                 } catch (IOException e) {
228                   throw new RuntimeException(e);
229                 }
230               }
231             });
232 
233     try {
234       future.get(IO_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
235     } catch (InterruptedException | ExecutionException | TimeoutException e) {
236       throw new IOException(e);
237     }
238   }
239 
240   /**
241    * Reads a frame from the shell's stdout, which has the format:
242    *
243    * <pre>{@code
244    * +---------------------+-----------------+
245    * | 4-bytes             | |length| bytes  |
246    * +---------------------+-----------------+
247    * | (unsigned) length   |     contents    |
248    * +---------------------+-----------------+
249    * }</pre>
250    *
251    * @return the contents that were read
252    * @throws IOException
253    */
readFrame()254   private byte[] readFrame() throws IOException {
255     if (shellProcess == null) {
256       throw new IllegalStateException("Shell not started.");
257     }
258 
259     InputStream inputStream = shellProcess.getInputStream();
260     byte[] lengthBytes = new byte[4];
261     if (inputStream.read(lengthBytes) != lengthBytes.length) {
262       throw new IOException("Failed to read length.");
263     }
264 
265     int length = ByteBuffer.wrap(lengthBytes).order(ByteOrder.BIG_ENDIAN).getInt();
266     if (length < 0) {
267       throw new IOException("Length too large: " + Arrays.toString(lengthBytes));
268     }
269 
270     byte[] contents = new byte[length];
271     int bytesRead = inputStream.read(contents);
272     if (bytesRead != length) {
273       throw new IOException("Failed to read entire contents: " + bytesRead + " != " + length);
274     }
275 
276     return contents;
277   }
278 
279   /**
280    * Writes a frame to the shell's stdin, which has the format:
281    *
282    * <pre>{@code
283    * +---------------------+-----------------+
284    * | 4-bytes             | |length| bytes  |
285    * +---------------------+-----------------+
286    * | (unsigned) length   |     contents    |
287    * +---------------------+-----------------+
288    * }</pre>
289    *
290    * @param contents the contents to send.
291    * @throws IOException
292    */
writeFrame(byte[] contents)293   private void writeFrame(byte[] contents) throws IOException {
294     if (shellProcess == null) {
295       throw new IllegalStateException("Shell not started.");
296     }
297 
298     // The length is big-endian encoded, network byte order.
299     long length = contents.length;
300     byte[] lengthBytes = new byte[4];
301     lengthBytes[0] = (byte) (length >> 32 & 0xFF);
302     lengthBytes[1] = (byte) (length >> 16 & 0xFF);
303     lengthBytes[2] = (byte) (length >> 8 & 0xFF);
304     lengthBytes[3] = (byte) (length >> 0 & 0xFF);
305 
306     OutputStream outputStream = shellProcess.getOutputStream();
307     outputStream.write(lengthBytes);
308     outputStream.write(contents);
309     outputStream.flush();
310   }
311 
312   /**
313    * Creates an expression to be processed when a secure connection is established, after the
314    * handshake is done.
315    *
316    * @param command The command to send.
317    * @param argument The argument of the command. Can be null.
318    * @return the expression that can be sent to the shell.
319    * @throws IOException.
320    */
createExpression(String command, @Nullable byte[] argument)321   private byte[] createExpression(String command, @Nullable byte[] argument) throws IOException {
322     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
323     outputStream.write(command.getBytes());
324     outputStream.write(" ".getBytes());
325     if (argument != null) {
326       outputStream.write(argument);
327     }
328     return outputStream.toByteArray();
329   }
330 
331   /** @return the mode string to use in the argument to start the ukey2_shell process. */
getModeString()332   private String getModeString() {
333     switch (mode) {
334       case INITIATOR:
335         return "initiator";
336       case RESPONDER:
337         return "responder";
338       default:
339         throw new IllegalArgumentException("Uknown mode " + mode);
340     }
341   }
342 }
343