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