summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/internal/net/VpnProfile.java293
-rw-r--r--tests/net/java/com/android/internal/net/VpnProfileTest.java185
2 files changed, 421 insertions, 57 deletions
diff --git a/core/java/com/android/internal/net/VpnProfile.java b/core/java/com/android/internal/net/VpnProfile.java
index 4bb012aac769..bbae0273ef4e 100644
--- a/core/java/com/android/internal/net/VpnProfile.java
+++ b/core/java/com/android/internal/net/VpnProfile.java
@@ -16,6 +16,7 @@
package com.android.internal.net;
+import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ProxyInfo;
import android.os.Build;
@@ -23,21 +24,34 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
/**
- * Parcel-like entity class for VPN profiles. To keep things simple, all
- * fields are package private. Methods are provided for serialization, so
- * storage can be implemented easily. Two rules are set for this class.
- * First, all fields must be kept non-null. Second, always make a copy
- * using clone() before modifying.
+ * Profile storage class for a platform VPN.
+ *
+ * <p>This class supports both the Legacy VPN, as well as application-configurable platform VPNs
+ * (such as IKEv2/IPsec).
+ *
+ * <p>This class is serialized and deserialized via the {@link #encode()} and {@link #decode()}
+ * functions for persistent storage in the Android Keystore. The encoding is entirely custom, but
+ * must be kept for backward compatibility for devices upgrading between Android versions.
*
* @hide
*/
-public class VpnProfile implements Cloneable, Parcelable {
+public final class VpnProfile implements Cloneable, Parcelable {
private static final String TAG = "VpnProfile";
+ @VisibleForTesting static final String VALUE_DELIMITER = "\0";
+ @VisibleForTesting static final String LIST_DELIMITER = ",";
+
// Match these constants with R.array.vpn_types.
public static final int TYPE_PPTP = 0;
public static final int TYPE_L2TP_IPSEC_PSK = 1;
@@ -45,39 +59,85 @@ public class VpnProfile implements Cloneable, Parcelable {
public static final int TYPE_IPSEC_XAUTH_PSK = 3;
public static final int TYPE_IPSEC_XAUTH_RSA = 4;
public static final int TYPE_IPSEC_HYBRID_RSA = 5;
- public static final int TYPE_MAX = 5;
+ public static final int TYPE_IKEV2_IPSEC_USER_PASS = 6;
+ public static final int TYPE_IKEV2_IPSEC_PSK = 7;
+ public static final int TYPE_IKEV2_IPSEC_RSA = 8;
+ public static final int TYPE_MAX = 8;
// Match these constants with R.array.vpn_proxy_settings.
public static final int PROXY_NONE = 0;
public static final int PROXY_MANUAL = 1;
+ private static final String ENCODED_NULL_PROXY_INFO = "\0\0\0\0";
+
// Entity fields.
@UnsupportedAppUsage
- public final String key; // -1
+ public final String key; // -1
+
@UnsupportedAppUsage
- public String name = ""; // 0
+ public String name = ""; // 0
+
@UnsupportedAppUsage
- public int type = TYPE_PPTP; // 1
+ public int type = TYPE_PPTP; // 1
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
- public String server = ""; // 2
+ public String server = ""; // 2
+
@UnsupportedAppUsage
- public String username = ""; // 3
- public String password = ""; // 4
- public String dnsServers = ""; // 5
- public String searchDomains = ""; // 6
- public String routes = ""; // 7
- public boolean mppe = true; // 8
- public String l2tpSecret = ""; // 9
- public String ipsecIdentifier = "";// 10
- public String ipsecSecret = ""; // 11
- public String ipsecUserCert = ""; // 12
- public String ipsecCaCert = ""; // 13
- public String ipsecServerCert = "";// 14
- public ProxyInfo proxy = null; // 15~18
+ public String username = ""; // 3
+ public String password = ""; // 4
+ public String dnsServers = ""; // 5
+ public String searchDomains = ""; // 6
+ public String routes = ""; // 7
+ public boolean mppe = true; // 8
+ public String l2tpSecret = ""; // 9
+ public String ipsecIdentifier = ""; // 10
+
+ /**
+ * The RSA private key or pre-shared key used for authentication.
+ *
+ * <p>If areAuthParamsInline is {@code true}, this String will be either:
+ *
+ * <ul>
+ * <li>If this is an IKEv2 RSA profile: a PKCS#8 encoded {@link java.security.PrivateKey}
+ * <li>If this is an IKEv2 PSK profile: a string value representing the PSK.
+ * </ul>
+ */
+ public String ipsecSecret = ""; // 11
+
+ /**
+ * The RSA certificate to be used for digital signature authentication.
+ *
+ * <p>If areAuthParamsInline is {@code true}, this String will be a pem-encoded {@link
+ * java.security.X509Certificate}
+ */
+ public String ipsecUserCert = ""; // 12
+
+ /**
+ * The RSA certificate that should be used to verify the server's end/target certificate.
+ *
+ * <p>If areAuthParamsInline is {@code true}, this String will be a pem-encoded {@link
+ * java.security.X509Certificate}
+ */
+ public String ipsecCaCert = ""; // 13
+ public String ipsecServerCert = ""; // 14
+ public ProxyInfo proxy = null; // 15~18
+
+ /**
+ * The list of allowable algorithms.
+ *
+ * <p>This list is validated in the setter to ensure that encoding characters (list, value
+ * delimiters) are not present in the algorithm names. See {@link #validateAllowedAlgorithms()}
+ */
+ private List<String> mAllowedAlgorithms = new ArrayList<>(); // 19
+ public boolean isBypassable = false; // 20
+ public boolean isMetered = false; // 21
+ public int maxMtu = 1400; // 22
+ public boolean areAuthParamsInline = false; // 23
// Helper fields.
@UnsupportedAppUsage
- public boolean saveLogin = false;
+ public transient boolean saveLogin = false;
public VpnProfile(String key) {
this.key = key;
@@ -103,6 +163,34 @@ public class VpnProfile implements Cloneable, Parcelable {
ipsecServerCert = in.readString();
saveLogin = in.readInt() != 0;
proxy = in.readParcelable(null);
+ mAllowedAlgorithms = new ArrayList<>();
+ in.readList(mAllowedAlgorithms, null);
+ isBypassable = in.readBoolean();
+ isMetered = in.readBoolean();
+ maxMtu = in.readInt();
+ areAuthParamsInline = in.readBoolean();
+ }
+
+ /**
+ * Retrieves the list of allowed algorithms.
+ *
+ * <p>The contained elements are as listed in {@link IpSecAlgorithm}
+ */
+ public List<String> getAllowedAlgorithms() {
+ return Collections.unmodifiableList(mAllowedAlgorithms);
+ }
+
+ /**
+ * Validates and sets the list of algorithms that can be used for the IPsec transforms.
+ *
+ * @param allowedAlgorithms the list of allowable algorithms, as listed in {@link
+ * IpSecAlgorithm}.
+ * @throws IllegalArgumentException if any delimiters are used in algorithm names. See {@link
+ * #VALUE_DELIMITER} and {@link LIST_DELIMITER}.
+ */
+ public void setAllowedAlgorithms(List<String> allowedAlgorithms) {
+ validateAllowedAlgorithms(allowedAlgorithms);
+ mAllowedAlgorithms = allowedAlgorithms;
}
@Override
@@ -125,8 +213,18 @@ public class VpnProfile implements Cloneable, Parcelable {
out.writeString(ipsecServerCert);
out.writeInt(saveLogin ? 1 : 0);
out.writeParcelable(proxy, flags);
+ out.writeList(mAllowedAlgorithms);
+ out.writeBoolean(isBypassable);
+ out.writeBoolean(isMetered);
+ out.writeInt(maxMtu);
+ out.writeBoolean(areAuthParamsInline);
}
+ /**
+ * Decodes a VpnProfile instance from the encoded byte array.
+ *
+ * <p>See {@link #encode()}
+ */
@UnsupportedAppUsage
public static VpnProfile decode(String key, byte[] value) {
try {
@@ -134,9 +232,11 @@ public class VpnProfile implements Cloneable, Parcelable {
return null;
}
- String[] values = new String(value, StandardCharsets.UTF_8).split("\0", -1);
- // There can be 14 - 19 Bytes in values.length.
- if (values.length < 14 || values.length > 19) {
+ String[] values = new String(value, StandardCharsets.UTF_8).split(VALUE_DELIMITER, -1);
+ // Acceptable numbers of values are:
+ // 14-19: Standard profile, with option for serverCert, proxy
+ // 24: Standard profile with serverCert, proxy and platform-VPN parameters.
+ if ((values.length < 14 || values.length > 19) && values.length != 24) {
return null;
}
@@ -164,13 +264,23 @@ public class VpnProfile implements Cloneable, Parcelable {
String port = (values.length > 16) ? values[16] : "";
String exclList = (values.length > 17) ? values[17] : "";
String pacFileUrl = (values.length > 18) ? values[18] : "";
- if (pacFileUrl.isEmpty()) {
+ if (!host.isEmpty() || !port.isEmpty() || !exclList.isEmpty()) {
profile.proxy = new ProxyInfo(host, port.isEmpty() ?
0 : Integer.parseInt(port), exclList);
- } else {
+ } else if (!pacFileUrl.isEmpty()) {
profile.proxy = new ProxyInfo(pacFileUrl);
}
- } // else profle.proxy = null
+ } // else profile.proxy = null
+
+ // Either all must be present, or none must be.
+ if (values.length >= 24) {
+ profile.mAllowedAlgorithms = Arrays.asList(values[19].split(LIST_DELIMITER));
+ profile.isBypassable = Boolean.parseBoolean(values[20]);
+ profile.isMetered = Boolean.parseBoolean(values[21]);
+ profile.maxMtu = Integer.parseInt(values[22]);
+ profile.areAuthParamsInline = Boolean.parseBoolean(values[23]);
+ }
+
profile.saveLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
return profile;
} catch (Exception e) {
@@ -179,36 +289,52 @@ public class VpnProfile implements Cloneable, Parcelable {
return null;
}
+ /**
+ * Encodes a VpnProfile instance to a byte array for storage.
+ *
+ * <p>See {@link #decode(String, byte[])}
+ */
public byte[] encode() {
StringBuilder builder = new StringBuilder(name);
- builder.append('\0').append(type);
- builder.append('\0').append(server);
- builder.append('\0').append(saveLogin ? username : "");
- builder.append('\0').append(saveLogin ? password : "");
- builder.append('\0').append(dnsServers);
- builder.append('\0').append(searchDomains);
- builder.append('\0').append(routes);
- builder.append('\0').append(mppe);
- builder.append('\0').append(l2tpSecret);
- builder.append('\0').append(ipsecIdentifier);
- builder.append('\0').append(ipsecSecret);
- builder.append('\0').append(ipsecUserCert);
- builder.append('\0').append(ipsecCaCert);
- builder.append('\0').append(ipsecServerCert);
+ builder.append(VALUE_DELIMITER).append(type);
+ builder.append(VALUE_DELIMITER).append(server);
+ builder.append(VALUE_DELIMITER).append(saveLogin ? username : "");
+ builder.append(VALUE_DELIMITER).append(saveLogin ? password : "");
+ builder.append(VALUE_DELIMITER).append(dnsServers);
+ builder.append(VALUE_DELIMITER).append(searchDomains);
+ builder.append(VALUE_DELIMITER).append(routes);
+ builder.append(VALUE_DELIMITER).append(mppe);
+ builder.append(VALUE_DELIMITER).append(l2tpSecret);
+ builder.append(VALUE_DELIMITER).append(ipsecIdentifier);
+ builder.append(VALUE_DELIMITER).append(ipsecSecret);
+ builder.append(VALUE_DELIMITER).append(ipsecUserCert);
+ builder.append(VALUE_DELIMITER).append(ipsecCaCert);
+ builder.append(VALUE_DELIMITER).append(ipsecServerCert);
if (proxy != null) {
- builder.append('\0').append(proxy.getHost() != null ? proxy.getHost() : "");
- builder.append('\0').append(proxy.getPort());
- builder.append('\0').append(proxy.getExclusionListAsString() != null ?
- proxy.getExclusionListAsString() : "");
- builder.append('\0').append(proxy.getPacFileUrl().toString());
+ builder.append(VALUE_DELIMITER).append(proxy.getHost() != null ? proxy.getHost() : "");
+ builder.append(VALUE_DELIMITER).append(proxy.getPort());
+ builder.append(VALUE_DELIMITER)
+ .append(
+ proxy.getExclusionListAsString() != null
+ ? proxy.getExclusionListAsString()
+ : "");
+ builder.append(VALUE_DELIMITER).append(proxy.getPacFileUrl().toString());
+ } else {
+ builder.append(ENCODED_NULL_PROXY_INFO);
}
+
+ builder.append(VALUE_DELIMITER).append(String.join(LIST_DELIMITER, mAllowedAlgorithms));
+ builder.append(VALUE_DELIMITER).append(isBypassable);
+ builder.append(VALUE_DELIMITER).append(isMetered);
+ builder.append(VALUE_DELIMITER).append(maxMtu);
+ builder.append(VALUE_DELIMITER).append(areAuthParamsInline);
+
return builder.toString().getBytes(StandardCharsets.UTF_8);
}
/**
- * Tests if profile is valid for lockdown, which requires IPv4 address for
- * both server and DNS. Server hostnames would require using DNS before
- * connection.
+ * Tests if profile is valid for lockdown, which requires IPv4 address for both server and DNS.
+ * Server hostnames would require using DNS before connection.
*/
public boolean isValidLockdownProfile() {
return isTypeValidForLockdown()
@@ -238,10 +364,7 @@ public class VpnProfile implements Cloneable, Parcelable {
return !TextUtils.isEmpty(dnsServers);
}
- /**
- * Returns {@code true} if all DNS servers have numeric addresses,
- * e.g. 8.8.8.8
- */
+ /** Returns {@code true} if all DNS servers have numeric addresses, e.g. 8.8.8.8 */
public boolean areDnsAddressesNumeric() {
try {
for (String dnsServer : dnsServers.split(" +")) {
@@ -253,6 +376,62 @@ public class VpnProfile implements Cloneable, Parcelable {
return true;
}
+ /**
+ * Validates that the provided list of algorithms does not contain illegal characters.
+ *
+ * @param allowedAlgorithms The list to be validated
+ */
+ public static void validateAllowedAlgorithms(List<String> allowedAlgorithms) {
+ for (final String alg : allowedAlgorithms) {
+ if (alg.contains(VALUE_DELIMITER) || alg.contains(LIST_DELIMITER)) {
+ throw new IllegalArgumentException(
+ "Algorithm contained illegal ('\0' or ',') character");
+ }
+ }
+ }
+
+ /** Generates a hashcode over the VpnProfile. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ key, type, server, username, password, dnsServers, searchDomains, routes, mppe,
+ l2tpSecret, ipsecIdentifier, ipsecSecret, ipsecUserCert, ipsecCaCert, ipsecServerCert,
+ proxy, mAllowedAlgorithms, isBypassable, isMetered, maxMtu, areAuthParamsInline);
+ }
+
+ /** Checks VPN profiles for interior equality. */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof VpnProfile)) {
+ return false;
+ }
+
+ final VpnProfile other = (VpnProfile) obj;
+ return Objects.equals(key, other.key)
+ && Objects.equals(name, other.name)
+ && type == other.type
+ && Objects.equals(server, other.server)
+ && Objects.equals(username, other.username)
+ && Objects.equals(password, other.password)
+ && Objects.equals(dnsServers, other.dnsServers)
+ && Objects.equals(searchDomains, other.searchDomains)
+ && Objects.equals(routes, other.routes)
+ && mppe == other.mppe
+ && Objects.equals(l2tpSecret, other.l2tpSecret)
+ && Objects.equals(ipsecIdentifier, other.ipsecIdentifier)
+ && Objects.equals(ipsecSecret, other.ipsecSecret)
+ && Objects.equals(ipsecUserCert, other.ipsecUserCert)
+ && Objects.equals(ipsecCaCert, other.ipsecCaCert)
+ && Objects.equals(ipsecServerCert, other.ipsecServerCert)
+ && Objects.equals(proxy, other.proxy)
+ && Objects.equals(mAllowedAlgorithms, other.mAllowedAlgorithms)
+ && isBypassable == other.isBypassable
+ && isMetered == other.isMetered
+ && maxMtu == other.maxMtu
+ && areAuthParamsInline == other.areAuthParamsInline;
+ }
+
+ @NonNull
public static final Creator<VpnProfile> CREATOR = new Creator<VpnProfile>() {
@Override
public VpnProfile createFromParcel(Parcel in) {
diff --git a/tests/net/java/com/android/internal/net/VpnProfileTest.java b/tests/net/java/com/android/internal/net/VpnProfileTest.java
new file mode 100644
index 000000000000..8a4b53343c26
--- /dev/null
+++ b/tests/net/java/com/android/internal/net/VpnProfileTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.net;
+
+import static com.android.testutils.ParcelUtilsKt.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecAlgorithm;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+/** Unit tests for {@link VpnProfile}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public class VpnProfileTest {
+ private static final String DUMMY_PROFILE_KEY = "Test";
+
+ @Test
+ public void testDefaults() throws Exception {
+ final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
+
+ assertEquals(DUMMY_PROFILE_KEY, p.key);
+ assertEquals("", p.name);
+ assertEquals(VpnProfile.TYPE_PPTP, p.type);
+ assertEquals("", p.server);
+ assertEquals("", p.username);
+ assertEquals("", p.password);
+ assertEquals("", p.dnsServers);
+ assertEquals("", p.searchDomains);
+ assertEquals("", p.routes);
+ assertTrue(p.mppe);
+ assertEquals("", p.l2tpSecret);
+ assertEquals("", p.ipsecIdentifier);
+ assertEquals("", p.ipsecSecret);
+ assertEquals("", p.ipsecUserCert);
+ assertEquals("", p.ipsecCaCert);
+ assertEquals("", p.ipsecServerCert);
+ assertEquals(null, p.proxy);
+ assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
+ assertFalse(p.isBypassable);
+ assertFalse(p.isMetered);
+ assertEquals(1400, p.maxMtu);
+ assertFalse(p.areAuthParamsInline);
+ }
+
+ private VpnProfile getSampleIkev2Profile(String key) {
+ final VpnProfile p = new VpnProfile(key);
+
+ p.name = "foo";
+ p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
+ p.server = "bar";
+ p.username = "baz";
+ p.password = "qux";
+ p.dnsServers = "8.8.8.8";
+ p.searchDomains = "";
+ p.routes = "0.0.0.0/0";
+ p.mppe = false;
+ p.l2tpSecret = "";
+ p.ipsecIdentifier = "quux";
+ p.ipsecSecret = "quuz";
+ p.ipsecUserCert = "corge";
+ p.ipsecCaCert = "grault";
+ p.ipsecServerCert = "garply";
+ p.proxy = null;
+ p.setAllowedAlgorithms(
+ Arrays.asList(
+ IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+ IpSecAlgorithm.AUTH_HMAC_SHA512,
+ IpSecAlgorithm.CRYPT_AES_CBC));
+ p.isBypassable = true;
+ p.isMetered = true;
+ p.maxMtu = 1350;
+ p.areAuthParamsInline = true;
+
+ // Not saved, but also not compared.
+ p.saveLogin = true;
+
+ return p;
+ }
+
+ @Test
+ public void testEquals() {
+ assertEquals(
+ getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));
+
+ final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+ modified.maxMtu--;
+ assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
+ }
+
+ @Test
+ public void testParcelUnparcel() {
+ assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 22);
+ }
+
+ @Test
+ public void testSetInvalidAlgorithmValueDelimiter() {
+ final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+
+ try {
+ profile.setAllowedAlgorithms(
+ Arrays.asList("test" + VpnProfile.VALUE_DELIMITER + "test"));
+ fail("Expected failure due to value separator in algorithm name");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void testSetInvalidAlgorithmListDelimiter() {
+ final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+
+ try {
+ profile.setAllowedAlgorithms(
+ Arrays.asList("test" + VpnProfile.LIST_DELIMITER + "test"));
+ fail("Expected failure due to value separator in algorithm name");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void testEncodeDecode() {
+ final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+ final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+ assertEquals(profile, decoded);
+ }
+
+ @Test
+ public void testEncodeDecodeTooManyValues() {
+ final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+ final byte[] tooManyValues =
+ (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();
+
+ assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
+ }
+
+ @Test
+ public void testEncodeDecodeInvalidNumberOfValues() {
+ final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+ final String encoded = new String(profile.encode());
+ final byte[] tooFewValues =
+ encoded.substring(0, encoded.lastIndexOf(VpnProfile.VALUE_DELIMITER)).getBytes();
+
+ assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues));
+ }
+
+ @Test
+ public void testEncodeDecodeLoginsNotSaved() {
+ final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+ profile.saveLogin = false;
+
+ final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+ assertNotEquals(profile, decoded);
+
+ // Add the username/password back, everything else must be equal.
+ decoded.username = profile.username;
+ decoded.password = profile.password;
+ assertEquals(profile, decoded);
+ }
+}