From d1e85633fbe8d871355d2b9feb51e2c9983d8a21 Mon Sep 17 00:00:00 2001 From: Samuel Holland Date: Wed, 5 Sep 2018 20:17:14 -0500 Subject: Remodel the Model - The configuration and crypto model is now entirely independent of Android classes other than Nullable and TextUtils. - Model classes are immutable and use builders that enforce the appropriate optional/required attributes. - The Android config proxies (for Parcelable and databinding) are moved to the Android side of the codebase, and are designed to be safe for two-way databinding. This allows proper observability in TunnelDetailFragment. - Various robustness fixes and documentation updates to helper classes. Signed-off-by: Jason A. Donenfeld --- app/src/main/java/com/wireguard/crypto/Key.java | 255 +++++++++++++++++++++ .../java/com/wireguard/crypto/KeyEncoding.java | 161 ------------- .../main/java/com/wireguard/crypto/KeyPair.java | 81 +++++++ .../main/java/com/wireguard/crypto/Keypair.java | 55 ----- 4 files changed, 336 insertions(+), 216 deletions(-) create mode 100644 app/src/main/java/com/wireguard/crypto/Key.java delete mode 100644 app/src/main/java/com/wireguard/crypto/KeyEncoding.java create mode 100644 app/src/main/java/com/wireguard/crypto/KeyPair.java delete mode 100644 app/src/main/java/com/wireguard/crypto/Keypair.java (limited to 'app/src/main/java/com/wireguard/crypto') diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/app/src/main/java/com/wireguard/crypto/Key.java new file mode 100644 index 00000000..85146794 --- /dev/null +++ b/app/src/main/java/com/wireguard/crypto/Key.java @@ -0,0 +1,255 @@ +/* + * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.crypto; + +import java.util.Arrays; + +/** + * Represents a WireGuard public or private key. This class uses specialized constant-time base64 + * and hexadecimal codec implementations that resist side-channel attacks. + *

+ * Instances of this class are immutable. + */ +@SuppressWarnings("MagicNumber") +public final class Key { + private final byte[] key; + + /** + * Constructs an object encapsulating the supplied key. + * + * @param key an array of bytes containing a binary key. Callers of this constructor are + * responsible for ensuring that the array is of the correct length. + */ + private Key(final byte[] key) { + // Defensively copy to ensure immutability. + this.key = Arrays.copyOf(key, key.length); + } + + /** + * Decodes a single 4-character base64 chunk to an integer in constant time. + * + * @param src an array of at least 4 characters in base64 format + * @param srcOffset the offset of the beginning of the chunk in {@code src} + * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not + * valid base64 + */ + private static int decodeBase64(final char[] src, final int srcOffset) { + int val = 0; + for (int i = 0; i < 4; ++i) { + final char c = src[i + srcOffset]; + val |= (-1 + + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64)) + + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70)) + + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5)) + + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63) + + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64) + ) << (18 - 6 * i); + } + return val; + } + + /** + * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time. + * + * @param src an array of at least 3 bytes + * @param srcOffset the offset of the beginning of the chunk in {@code src} + * @param dest an array of at least 4 characters + * @param destOffset the offset of the beginning of the chunk in {@code dest} + */ + private static void encodeBase64(final byte[] src, final int srcOffset, + final char[] dest, final int destOffset) { + final byte[] input = { + (byte) ((src[srcOffset] >>> 2) & 63), + (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63), + (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63), + (byte) ((src[2 + srcOffset]) & 63), + }; + for (int i = 0; i < 4; ++i) { + dest[i + destOffset] = (char) (input[i] + 'A' + + (((25 - input[i]) >>> 8) & 6) + - (((51 - input[i]) >>> 8) & 75) + - (((61 - input[i]) >>> 8) & 15) + + (((62 - input[i]) >>> 8) & 3)); + } + } + + /** + * Decodes a WireGuard public or private key from its base64 string representation. This + * function throws a {@link KeyFormatException} if the source string is not well-formed. + * + * @param str the base64 string representation of a WireGuard key + * @return the decoded key encapsulated in an immutable container + */ + public static Key fromBase64(final String str) { + final char[] input = str.toCharArray(); + if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') + throw new KeyFormatException(Format.BASE64); + final byte[] key = new byte[Format.BINARY.length]; + int i; + int ret = 0; + for (i = 0; i < key.length / 3; ++i) { + final int val = decodeBase64(input, i * 4); + ret |= val >>> 31; + key[i * 3] = (byte) ((val >>> 16) & 0xff); + key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); + key[i * 3 + 2] = (byte) (val & 0xff); + } + final char[] endSegment = { + input[i * 4], + input[i * 4 + 1], + input[i * 4 + 2], + 'A', + }; + final int val = decodeBase64(endSegment, 0); + ret |= (val >>> 31) | (val & 0xff); + key[i * 3] = (byte) ((val >>> 16) & 0xff); + key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); + + if (ret != 0) + throw new KeyFormatException(Format.BASE64); + return new Key(key); + } + + /** + * Wraps a WireGuard public or private key in an immutable container. This function throws a + * {@link KeyFormatException} if the source data is not the correct length. + * + * @param bytes an array of bytes containing a WireGuard key in binary format + * @return the key encapsulated in an immutable container + */ + public static Key fromBytes(final byte[] bytes) { + if (bytes.length != Format.BINARY.length) + throw new KeyFormatException(Format.BINARY); + return new Key(bytes); + } + + /** + * Decodes a WireGuard public or private key from its hexadecimal string representation. This + * function throws a {@link KeyFormatException} if the source string is not well-formed. + * + * @param str the hexadecimal string representation of a WireGuard key + * @return the decoded key encapsulated in an immutable container + */ + public static Key fromHex(final String str) { + final char[] input = str.toCharArray(); + if (input.length != Format.HEX.length) + throw new KeyFormatException(Format.HEX); + final byte[] key = new byte[Format.BINARY.length]; + int ret = 0; + for (int i = 0; i < key.length; ++i) { + int c; + int cNum; + int cNum0; + int cAlpha; + int cAlpha0; + int cVal; + final int cAcc; + + c = input[i * 2]; + cNum = c ^ 48; + cNum0 = ((cNum - 10) >>> 8) & 0xff; + cAlpha = (c & ~32) - 55; + cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; + ret |= ((cNum0 | cAlpha0) - 1) >>> 8; + cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); + cAcc = cVal * 16; + + c = input[i * 2 + 1]; + cNum = c ^ 48; + cNum0 = ((cNum - 10) >>> 8) & 0xff; + cAlpha = (c & ~32) - 55; + cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; + ret |= ((cNum0 | cAlpha0) - 1) >>> 8; + cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); + key[i] = (byte) (cAcc | cVal); + } + if (ret != 0) + throw new KeyFormatException(Format.HEX); + return new Key(key); + } + + /** + * Returns the key as an array of bytes. + * + * @return an array of bytes containing the raw binary key + */ + public byte[] getBytes() { + // Defensively copy to ensure immutability. + return Arrays.copyOf(key, key.length); + } + + /** + * Encodes the key to base64. + * + * @return a string containing the encoded key + */ + public String toBase64() { + final char[] output = new char[Format.BASE64.length]; + int i; + for (i = 0; i < key.length / 3; ++i) + encodeBase64(key, i * 3, output, i * 4); + final byte[] endSegment = { + key[i * 3], + key[i * 3 + 1], + 0, + }; + encodeBase64(endSegment, 0, output, i * 4); + output[Format.BASE64.length - 1] = '='; + return new String(output); + } + + /** + * Encodes the key to hexadecimal ASCII characters. + * + * @return a string containing the encoded key + */ + public String toHex() { + final char[] output = new char[Format.HEX.length]; + for (int i = 0; i < key.length; ++i) { + output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf) + + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38)); + output[i * 2 + 1] = (char) (87 + (key[i] & 0xf) + + ((((key[i] & 0xf) - 10) >> 8) & ~38)); + } + return new String(output); + } + + /** + * The supported formats for encoding a WireGuard key. + */ + public enum Format { + BASE64(44), + BINARY(32), + HEX(64); + + private final int length; + + Format(final int length) { + this.length = length; + } + + public int getLength() { + return length; + } + } + + /** + * An exception thrown when attempting to parse an invalid key (too short, too long, or byte + * data inappropriate for the format). The format being parsed can be accessed with the + * {@link #getFormat} method. + */ + public static final class KeyFormatException extends RuntimeException { + private final Format format; + + private KeyFormatException(final Format format) { + this.format = format; + } + + public Format getFormat() { + return format; + } + } +} diff --git a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java b/app/src/main/java/com/wireguard/crypto/KeyEncoding.java deleted file mode 100644 index d29c2d44..00000000 --- a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -import com.wireguard.android.Application; -import com.wireguard.android.R; - -/** - * This is a specialized constant-time base64 and hex implementation that resists side-channel attacks. - */ - -@SuppressWarnings("MagicNumber") -public final class KeyEncoding { - public static final int KEY_LENGTH = 32; - public static final int KEY_LENGTH_BASE64 = 44; - public static final int KEY_LENGTH_HEX = 64; - private static final String KEY_LENGTH_BASE64_EXCEPTION_MESSAGE = - Application.get().getString(R.string.key_length_base64_exception_message); - private static final String KEY_LENGTH_EXCEPTION_MESSAGE = - Application.get().getString(R.string.key_length_exception_message); - private static final String KEY_LENGTH_HEX_EXCEPTION_MESSAGE = - Application.get().getString(R.string.key_length_hex_exception_message); - - private KeyEncoding() { - // Prevent instantiation. - } - - private static int decodeBase64(final char[] src, final int srcOffset) { - int val = 0; - for (int i = 0; i < 4; ++i) { - final char c = src[i + srcOffset]; - val |= (-1 - + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64)) - + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70)) - + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5)) - + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63) - + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64) - ) << (18 - 6 * i); - } - return val; - } - - private static void encodeBase64(final byte[] src, final int srcOffset, - final char[] dest, final int destOffset) { - final byte[] input = { - (byte) ((src[srcOffset] >>> 2) & 63), - (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63), - (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63), - (byte) ((src[2 + srcOffset]) & 63), - }; - for (int i = 0; i < 4; ++i) { - dest[i + destOffset] = (char) (input[i] + 'A' - + (((25 - input[i]) >>> 8) & 6) - - (((51 - input[i]) >>> 8) & 75) - - (((61 - input[i]) >>> 8) & 15) - + (((62 - input[i]) >>> 8) & 3)); - } - } - - public static byte[] keyFromBase64(final String str) { - final char[] input = str.toCharArray(); - final byte[] key = new byte[KEY_LENGTH]; - if (input.length != KEY_LENGTH_BASE64 || input[KEY_LENGTH_BASE64 - 1] != '=') - throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE); - int i; - int ret = 0; - for (i = 0; i < KEY_LENGTH / 3; ++i) { - final int val = decodeBase64(input, i * 4); - ret |= val >>> 31; - key[i * 3] = (byte) ((val >>> 16) & 0xff); - key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); - key[i * 3 + 2] = (byte) (val & 0xff); - } - final char[] endSegment = { - input[i * 4], - input[i * 4 + 1], - input[i * 4 + 2], - 'A', - }; - final int val = decodeBase64(endSegment, 0); - ret |= (val >>> 31) | (val & 0xff); - key[i * 3] = (byte) ((val >>> 16) & 0xff); - key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); - - if (ret != 0) - throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE); - return key; - } - - public static byte[] keyFromHex(final String str) { - final char[] input = str.toCharArray(); - final byte[] key = new byte[KEY_LENGTH]; - if (input.length != KEY_LENGTH_HEX) - throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE); - int ret = 0; - - for (int i = 0; i < KEY_LENGTH_HEX; i += 2) { - int c; - int cNum; - int cNum0; - int cAlpha; - int cAlpha0; - int cVal; - final int cAcc; - - c = input[i]; - cNum = c ^ 48; - cNum0 = ((cNum - 10) >>> 8) & 0xff; - cAlpha = (c & ~32) - 55; - cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; - ret |= ((cNum0 | cAlpha0) - 1) >>> 8; - cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); - cAcc = cVal * 16; - - c = input[i + 1]; - cNum = c ^ 48; - cNum0 = ((cNum - 10) >>> 8) & 0xff; - cAlpha = (c & ~32) - 55; - cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; - ret |= ((cNum0 | cAlpha0) - 1) >>> 8; - cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); - key[i / 2] = (byte) (cAcc | cVal); - } - if (ret != 0) - throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE); - return key; - } - - public static String keyToBase64(final byte[] key) { - final char[] output = new char[KEY_LENGTH_BASE64]; - if (key.length != KEY_LENGTH) - throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE); - int i; - for (i = 0; i < KEY_LENGTH / 3; ++i) - encodeBase64(key, i * 3, output, i * 4); - final byte[] endSegment = { - key[i * 3], - key[i * 3 + 1], - 0, - }; - encodeBase64(endSegment, 0, output, i * 4); - output[KEY_LENGTH_BASE64 - 1] = '='; - return new String(output); - } - - public static String keyToHex(final byte[] key) { - final char[] output = new char[KEY_LENGTH_HEX]; - if (key.length != KEY_LENGTH) - throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE); - for (int i = 0; i < KEY_LENGTH; ++i) { - output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf) - + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38)); - output[i * 2 + 1] = (char) (87 + (key[i] & 0xf) - + ((((key[i] & 0xf) - 10) >> 8) & ~38)); - } - return new String(output); - } -} diff --git a/app/src/main/java/com/wireguard/crypto/KeyPair.java b/app/src/main/java/com/wireguard/crypto/KeyPair.java new file mode 100644 index 00000000..2b2bf564 --- /dev/null +++ b/app/src/main/java/com/wireguard/crypto/KeyPair.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.crypto; + +import java.security.SecureRandom; + +/** + * Represents a Curve25519 key pair as used by WireGuard. + *

+ * Instances of this class are immutable. + */ +public class KeyPair { + private final Key privateKey; + private final Key publicKey; + + /** + * Creates a key pair using a newly-generated private key. + */ + public KeyPair() { + this(generatePrivateKey()); + } + + /** + * Creates a key pair using an existing private key. + * + * @param privateKey a private key, used to derive the public key + */ + public KeyPair(final Key privateKey) { + this.privateKey = privateKey; + publicKey = generatePublicKey(privateKey); + } + + /** + * Generates a private key using the system's {@link SecureRandom} number generator. + * + * @return a well-formed random private key + */ + @SuppressWarnings("MagicNumber") + private static Key generatePrivateKey() { + final SecureRandom secureRandom = new SecureRandom(); + final byte[] privateKey = new byte[Key.Format.BINARY.getLength()]; + secureRandom.nextBytes(privateKey); + privateKey[0] &= 248; + privateKey[31] &= 127; + privateKey[31] |= 64; + return Key.fromBytes(privateKey); + } + + /** + * Generates a public key from an existing private key. + * + * @param privateKey a private key + * @return a well-formed public key that corresponds to the supplied private key + */ + private static Key generatePublicKey(final Key privateKey) { + final byte[] publicKey = new byte[Key.Format.BINARY.getLength()]; + Curve25519.eval(publicKey, 0, privateKey.getBytes(), null); + return Key.fromBytes(publicKey); + } + + /** + * Returns the private key from the key pair. + * + * @return the private key + */ + public Key getPrivateKey() { + return privateKey; + } + + /** + * Returns the public key from the key pair. + * + * @return the public key + */ + public Key getPublicKey() { + return publicKey; + } +} diff --git a/app/src/main/java/com/wireguard/crypto/Keypair.java b/app/src/main/java/com/wireguard/crypto/Keypair.java deleted file mode 100644 index 0ee27542..00000000 --- a/app/src/main/java/com/wireguard/crypto/Keypair.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -import java.security.SecureRandom; - -/** - * Represents a Curve25519 keypair as used by WireGuard. - */ - -public class Keypair { - private final byte[] privateKey; - private final byte[] publicKey; - - public Keypair() { - this(generatePrivateKey()); - } - - private Keypair(final byte[] privateKey) { - this.privateKey = privateKey; - publicKey = generatePublicKey(privateKey); - } - - public Keypair(final String privateKey) { - this(KeyEncoding.keyFromBase64(privateKey)); - } - - @SuppressWarnings("MagicNumber") - private static byte[] generatePrivateKey() { - final SecureRandom secureRandom = new SecureRandom(); - final byte[] privateKey = new byte[KeyEncoding.KEY_LENGTH]; - secureRandom.nextBytes(privateKey); - privateKey[0] &= 248; - privateKey[31] &= 127; - privateKey[31] |= 64; - return privateKey; - } - - private static byte[] generatePublicKey(final byte[] privateKey) { - final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH]; - Curve25519.eval(publicKey, 0, privateKey, null); - return publicKey; - } - - public String getPrivateKey() { - return KeyEncoding.keyToBase64(privateKey); - } - - public String getPublicKey() { - return KeyEncoding.keyToBase64(publicKey); - } -} -- cgit v1.2.3