diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..34c77d8 --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,981 @@ + + + + + + \ No newline at end of file diff --git a/niap-cc/NIAPSEC/.gitignore b/niap-cc/NIAPSEC/.gitignore new file mode 100644 index 0000000..8d49328 --- /dev/null +++ b/niap-cc/NIAPSEC/.gitignore @@ -0,0 +1,35 @@ +# Android generated +bin +gen +libs +obj +lint.xml + +# Android Studio +.idea +.idea/ +*.iml +*.ipr +*.iws +classes +gen-external-apklibs + +# Gradle +.gradle +build +buildout +out + +# Maven +target +release.properties +pom.xml.* + +local.properties +proguard-rules.pro + +# Other +.DS_Store +dist +tmp/build + diff --git a/niap-cc/NIAPSEC/LICENSE b/niap-cc/NIAPSEC/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/niap-cc/NIAPSEC/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/niap-cc/NIAPSEC/README.md b/niap-cc/NIAPSEC/README.md new file mode 100644 index 0000000..54ce91f --- /dev/null +++ b/niap-cc/NIAPSEC/README.md @@ -0,0 +1,26 @@ +NIAPSEC - Security Extensions for NIAP on Android +======================== +NIAPSEC aims to provide an easy to use module that provides NIAP compliant functionality for key management, TLS, OCSP, and more. + +Experimental Release + +License +------- + +Copyright 2018 Google, Inc. + +Licensed to the Apache Software Foundation (ASF) under one or more contributor +license agreements. See the NOTICE file distributed with this work for +additional information regarding copyright ownership. The ASF licenses this +file to you 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. + diff --git a/niap-cc/NIAPSEC/build.gradle b/niap-cc/NIAPSEC/build.gradle new file mode 100644 index 0000000..89cee70 --- /dev/null +++ b/niap-cc/NIAPSEC/build.gradle @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 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. + */ + + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:8.8.2" + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven { url "https://jitpack.io" } + } +} + +apply plugin: 'com.android.library' + +android { + namespace="com.android.certifications.niap.niapsec" + compileSdk 36 + defaultConfig { + minSdkVersion 26 + //For robolectric test + //noinspection OldTargetApi + targetSdkVersion 34 + versionCode 3 + versionName "3.0.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility = '1.17' + targetCompatibility = '1.17' + } + testOptions { + unitTests { + includeAndroidResources = true + } + } + sourceSets { + main { + manifest.srcFile 'src/main/AndroidManifest.xml' + } + androidTest { + manifest.srcFile 'src/androidTest/AndroidManifest.xml' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'androidx.biometric:biometric:1.1.0' + implementation 'org.conscrypt:conscrypt-android:2.5.2' + implementation 'com.github.ChickenHook:RestrictionBypass:2.2' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'androidx.test:core:1.7.0' + testImplementation 'androidx.test.ext:junit:1.3.0' // AndroidX extensions for JUnit + testImplementation 'org.robolectric:robolectric:4.13' + + testImplementation 'org.mockito:mockito-core:5.12.0' + + // For instrumented tests (run on an Android device/emulator) + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' + // Note: The original android.support.test.runner is outdated. + // The line below is the modern replacement. + androidTestImplementation 'androidx.test:runner:1.7.0' + +} diff --git a/niap-cc/NIAPSEC/gradle.properties b/niap-cc/NIAPSEC/gradle.properties new file mode 100644 index 0000000..646c51b --- /dev/null +++ b/niap-cc/NIAPSEC/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.enableJetifier=true diff --git a/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.jar b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.jar differ diff --git a/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.properties b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..61e8111 --- /dev/null +++ b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Oct 31 10:55:08 JST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/niap-cc/NIAPSEC/gradlew b/niap-cc/NIAPSEC/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/niap-cc/NIAPSEC/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/niap-cc/NIAPSEC/gradlew.bat b/niap-cc/NIAPSEC/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/niap-cc/NIAPSEC/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/niap-cc/NIAPSEC/src/androidTest/AndroidManifest.xml b/niap-cc/NIAPSEC/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..859c04f --- /dev/null +++ b/niap-cc/NIAPSEC/src/androidTest/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/niap-cc/NIAPSEC/src/androidTest/java/com/android/certifications/niap/niapsec/net/RealConnectionTest.java b/niap-cc/NIAPSEC/src/androidTest/java/com/android/certifications/niap/niapsec/net/RealConnectionTest.java new file mode 100644 index 0000000..6ccb9d9 --- /dev/null +++ b/niap-cc/NIAPSEC/src/androidTest/java/com/android/certifications/niap/niapsec/net/RealConnectionTest.java @@ -0,0 +1,98 @@ +package com.android.certifications.niap.niapsec.net; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.content.Context; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** + * Instrumented test for real network connections using ValidatableSSLSocketFactory. + * This test requires an active internet connection and runs on an Android device or emulator. + */ +@RunWith(AndroidJUnit4.class) +public class RealConnectionTest { + + private Context context; + private SecureConfig secureConfig; + + @Before + public void setUp() { + // Get the context of the app under test. + context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + // Use the default secure configuration. + secureConfig = SecureConfig.getDefault(); + } + + /** + * Tests a successful connection to a trusted endpoint (google.com). + * The TLS handshake and certificate validation should complete without errors. + */ + @Test + public void connectToTrustedServer_shouldSucceed() { + HttpsURLConnection urlConnection = null; + + //We can use badssl.com for testing ssl https://badssl.com/ + //String trustedHost = "https://tls-v1-2.badssl.com:1012/"; + //String trustedHost = "https://www.google.com"; + //String trustedHost = "https://wikipedia.org"; + //revoked.grc.com + //String trustedHost = "https://revoked.grc.com"; + //String trustedHost = "https://www.grc.com"; + String trustedHost = "https://www.netscaler.com:443"; + try { + // 1. Create a SecureURL instance for a trusted site. + SecureURL secureURL = new SecureURL(trustedHost,null ,secureConfig); + + // 2. Create the custom SSLSocketFactory. + // This factory will perform the certificate validation. + SSLSocketFactory factory = new ValidatableSSLSocketFactory(secureURL); + + // 3. Create a URL object and open a standard HttpsURLConnection. + URL url = new URL(trustedHost); + urlConnection = (HttpsURLConnection) url.openConnection(); + + // 4. Set our custom factory on the connection. This is the key step. + urlConnection.setSSLSocketFactory(factory); + + // 5. Connect and get the response code. This triggers the handshake via our factory. + int responseCode = urlConnection.getResponseCode(); + + // 6. Read some data to ensure the connection is fully established. + InputStream in = urlConnection.getInputStream(); + byte[] buffer = new byte[1024]; + int bytesRead = in.read(buffer); + in.close(); + + // Assert that the connection was successful. + System.out.println("Successfully connected to google.com with response code: " + responseCode); + System.out.println("Read " + bytesRead + " bytes."); + + assertNotNull(in); + + } catch (IOException e) { + // If any IOException occurs, the test fails. + e.printStackTrace(); + fail("Connection to trusted server failed with IOException: " + e.getMessage()); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + +} \ No newline at end of file diff --git a/niap-cc/NIAPSEC/src/main/AndroidManifest.xml b/niap-cc/NIAPSEC/src/main/AndroidManifest.xml new file mode 100644 index 0000000..be0f4a2 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/SecureConfig.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/SecureConfig.java new file mode 100644 index 0000000..0564657 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/SecureConfig.java @@ -0,0 +1,696 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec; + +import android.security.keystore.KeyProperties; + +import com.android.certifications.niap.niapsec.biometric.BiometricSupport; +import com.android.certifications.niap.niapsec.config.TrustAnchorOptions; + +/** + * A global configuration object that shows configuration options for testing scenarios. + * + */ +public class SecureConfig { + + public static final String ANDROID_KEYSTORE = "AndroidKeyStore"; + public static final String ANDROID_CA_STORE = "AndroidCAStore"; + public static final int AES_IV_SIZE_BYTES = 12; + public static final String SSL_TLS = "TLS"; + public static String PACKAGE_NAME = "com.android.certifications.niap.niapsec"; + + private String androidKeyStore = SecureConfig.ANDROID_KEYSTORE; + private String androidCAStore = SecureConfig.ANDROID_CA_STORE; + private String keystoreType = "PKCS12"; + + // Asymmetric Encryption Constants + private String asymmetricKeyPairAlgorithm = KeyProperties.KEY_ALGORITHM_RSA; + private int asymmetricKeySize = 3072; + private int teeGCIterations = 10; + //private String asymmetricCipherTransformation = "RSA/ECB/PKCS1Padding"; + private String asymmetricCipherTransformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + private String asymmetricBlockModes = KeyProperties.BLOCK_MODE_ECB; + //private String asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1; + private String asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_OAEP; + private int asymmetricKeyPurposes = KeyProperties.PURPOSE_DECRYPT; + // Sets KeyGenBuilder#setUnlockedDeviceRequired to true, requires Android 9 Pie. + private boolean asymmetricSensitiveDataProtection = true; + private boolean asymmetricRequireUserAuth = true; + private int asymmetricRequireUserValiditySeconds = -1; + private String asymmetricDigests = KeyProperties.DIGEST_SHA256; + + // Symmetric Encryption Constants + private String symmetricKeyAlgorithm = KeyProperties.KEY_ALGORITHM_AES; + private String symmetricBlockModes = KeyProperties.BLOCK_MODE_GCM; + private String symmetricPaddings = KeyProperties.ENCRYPTION_PADDING_NONE; + private int symmetricKeySize = 256; + private int symmetricGcmTagLength = 128; + private int symmetricKeyPurposes = + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT; + private String symmetricCipherTransformation = "AES/GCM/NoPadding"; + // Sets KeyGenBuilder#setUnlockedDeviceRequired to true, requires Android 9 Pie. + private boolean symmetricSensitiveDataProtection = true; + private boolean symmetricRequireUserAuth = true; + private int symmetricRequireUserValiditySeconds = -1; + private String symmetricDigests = null; + + private boolean logging = false; + + // Certificate Constants + private String certPath = "X.509"; + private String certPathValidator = "PKIX"; + private boolean useStrongSSLCiphers = true; + private String[] strongSSLCiphers = new String[]{ + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + }; + private String[] clientCertAlgorithms = new String[]{"RSA"}; + private TrustAnchorOptions trustAnchorOptions = TrustAnchorOptions.USER_SYSTEM; + + private BiometricSupport biometricSupport = null; + + private SecureConfig() { + } + + /** + * Builder for SecureConfig + */ + public static class Builder { + + public Builder() { + } + + // Keystore Constants + private String androidKeyStore; + private String androidCAStore; + private String keystoreType; + + public Builder forKeyStoreType(String keystoreType) { + this.keystoreType = keystoreType; + return this; + } + + // Asymmetric Encryption Constants + private int teeGCIterations; + private String asymmetricKeyPairAlgorithm; + private int asymmetricKeySize; + private String asymmetricCipherTransformation; + private int asymmetricKeyPurposes; + private String asymmetricBlockModes; + private String asymmetricPaddings; + private boolean asymmetricSensitiveDataProtection; + private boolean asymmetricRequireUserAuth = true; + private int asymmetricRequireUserValiditySeconds = -1; + + public Builder setAsymmetricKeyPairAlgorithm(String keyPairAlgorithm) { + this.asymmetricKeyPairAlgorithm = keyPairAlgorithm; + return this; + } + + public Builder setAsymmetricKeySize(int keySize) { + this.asymmetricKeySize = keySize; + return this; + } + + public Builder setTeeGCIterations(int iterations) { + this.teeGCIterations = iterations; + return this; + } + + public Builder setAsymmetricCipherTransformation(String cipherTransformation) { + this.asymmetricCipherTransformation = cipherTransformation; + return this; + } + + public Builder setAsymmetricKeyPurposes(int purposes) { + this.asymmetricKeyPurposes = purposes; + return this; + } + + public Builder setAsymmetricBlockModes(String blockModes) { + this.asymmetricBlockModes = blockModes; + return this; + } + + public Builder setAsymmetricPaddings(String paddings) { + this.asymmetricPaddings = paddings; + return this; + } + + public Builder setAsymmetricSensitiveDataProtection(boolean dataProtection) { + this.asymmetricSensitiveDataProtection = dataProtection; + return this; + } + + public Builder setAsymmetricRequireUserAuth(boolean userAuth) { + this.asymmetricRequireUserAuth = userAuth; + return this; + } + + public Builder setAsymmetricRequireUserValiditySeconds(int authValiditySeconds) { + this.asymmetricRequireUserValiditySeconds = authValiditySeconds; + return this; + } + + // Symmetric Encryption Constants + private String symmetricKeyAlgorithm; + private String symmetricBlockModes; + private String symmetricPaddings; + private int symmetricKeySize; + private int symmetricGcmTagLength; + private int symmetricKeyPurposes; + private String symmetricCipherTransformation; + private boolean symmetricSensitiveDataProtection; + private boolean symmetricRequireUserAuth = true; + private int symmetricRequireUserValiditySeconds = -1; + + private boolean logging = false; + + public Builder setLoggingEnabled(boolean enabled) { + logging = enabled; + return this; + } + + public Builder setSymmetricKeyAlgorithm(String keyAlgorithm) { + this.symmetricKeyAlgorithm = keyAlgorithm; + return this; + } + + public Builder setSymmetricKeySize(int keySize) { + this.symmetricKeySize = keySize; + return this; + } + + public Builder setSymmetricCipherTransformation(String cipherTransformation) { + this.symmetricCipherTransformation = cipherTransformation; + return this; + } + + public Builder setSymmetricKeyPurposes(int purposes) { + this.symmetricKeyPurposes = purposes; + return this; + } + + public Builder setSymmetricGcmTagLength(int gcmTagLength) { + this.symmetricGcmTagLength = gcmTagLength; + return this; + } + + public Builder setSymmetricBlockModes(String blockModes) { + this.symmetricBlockModes = blockModes; + return this; + } + + public Builder setSymmetricPaddings(String paddings) { + this.symmetricPaddings = paddings; + return this; + } + + public Builder setSymmetricSensitiveDataProtection(boolean dataProtection) { + this.symmetricSensitiveDataProtection = dataProtection; + return this; + } + + public Builder setSymmetricRequireUserAuth(boolean userAuth) { + this.symmetricRequireUserAuth = userAuth; + return this; + } + + public Builder setSymmetricRequireUserValiditySeconds(int authValiditySeconds) { + this.symmetricRequireUserValiditySeconds = authValiditySeconds; + return this; + } + + // Certificate Constants + private String certPath; + private String certPathValidator; + private boolean useStrongSSLCiphers; + private String[] strongSSLCiphers; + private String[] clientCertAlgorithms; + private TrustAnchorOptions trustAnchorOptions; + private BiometricSupport biometricSupport; + + public Builder setCertPath(String certPath) { + this.certPath = certPath; + return this; + } + + public Builder setCertPathValidator(String certPathValidator) { + this.certPathValidator = certPathValidator; + return this; + } + + public Builder setUseStrongSSLCiphers(boolean strongSSLCiphers) { + this.useStrongSSLCiphers = strongSSLCiphers; + return this; + } + + public Builder setStrongSSLCiphers(String[] strongSSLCiphers) { + this.strongSSLCiphers = strongSSLCiphers; + return this; + } + + public Builder setClientCertAlgorithms(String[] clientCertAlgorithms) { + this.clientCertAlgorithms = clientCertAlgorithms; + return this; + } + + public Builder setTrustAnchorOptions(TrustAnchorOptions trustAnchorOptions) { + this.trustAnchorOptions = trustAnchorOptions; + return this; + } + + public Builder setBiometricSupport(BiometricSupport biometricSupport) { + this.biometricSupport = biometricSupport; + return this; + } + + public SecureConfig build() { + SecureConfig secureConfig = new SecureConfig(); + secureConfig.androidKeyStore = this.androidKeyStore; + secureConfig.androidCAStore = this.androidCAStore; + secureConfig.keystoreType = this.keystoreType; + + secureConfig.asymmetricKeyPairAlgorithm = this.asymmetricKeyPairAlgorithm; + secureConfig.asymmetricKeySize = this.asymmetricKeySize; + secureConfig.teeGCIterations = this.teeGCIterations; + secureConfig.asymmetricCipherTransformation = this.asymmetricCipherTransformation; + secureConfig.asymmetricKeyPurposes = this.asymmetricKeyPurposes; + secureConfig.asymmetricBlockModes = this.asymmetricBlockModes; + secureConfig.asymmetricPaddings = this.asymmetricPaddings; + secureConfig.asymmetricSensitiveDataProtection = + this.asymmetricSensitiveDataProtection; + secureConfig.asymmetricRequireUserAuth = this.asymmetricRequireUserAuth; + secureConfig.asymmetricRequireUserValiditySeconds = + this.asymmetricRequireUserValiditySeconds; + + secureConfig.symmetricKeyAlgorithm = this.symmetricKeyAlgorithm; + secureConfig.symmetricBlockModes = this.symmetricBlockModes; + secureConfig.symmetricPaddings = this.symmetricPaddings; + secureConfig.symmetricKeySize = this.symmetricKeySize; + secureConfig.symmetricGcmTagLength = this.symmetricGcmTagLength; + secureConfig.symmetricKeyPurposes = this.symmetricKeyPurposes; + secureConfig.symmetricCipherTransformation = this.symmetricCipherTransformation; + secureConfig.symmetricSensitiveDataProtection = this.symmetricSensitiveDataProtection; + secureConfig.symmetricRequireUserAuth = this.symmetricRequireUserAuth; + secureConfig.symmetricRequireUserValiditySeconds = + this.symmetricRequireUserValiditySeconds; + + secureConfig.certPath = this.certPath; + secureConfig.certPathValidator = this.certPathValidator; + secureConfig.useStrongSSLCiphers = this.useStrongSSLCiphers; + secureConfig.strongSSLCiphers = this.strongSSLCiphers; + secureConfig.clientCertAlgorithms = this.clientCertAlgorithms; + secureConfig.trustAnchorOptions = this.trustAnchorOptions; + secureConfig.biometricSupport = this.biometricSupport; + secureConfig.logging = this.logging; + + return secureConfig; + } + } + + /** + * Gets the default strong config + * + * @return The default config without biometric support with recommended NIAP settings + */ + public static SecureConfig getDefault() { + return getStrongConfig(null); + } + + /** + * Gets the default strong config with biometric support + * + * @param biometricSupport The callbacks + * @return A config with biometric support configured + */ + public static SecureConfig getDefault(BiometricSupport biometricSupport) { + return getStrongConfig(biometricSupport); + } + + /** + * Gets the default strong config + * + * @return Default config with the strongest encryption settings + */ + public static SecureConfig getStrongConfig() { + return getStrongConfig(null); + } + + /** + * Gets the default strong config with biometric support + * + * @param biometricSupport The biometric callback used for device credential + * @return A secure config with strong NIAP settings built in and biometric support + */ + public static SecureConfig getStrongDeviceCredentialConfig(BiometricSupport biometricSupport) { + SecureConfig secureConfig = getStrongConfig(biometricSupport); + secureConfig.asymmetricRequireUserValiditySeconds = 10; + secureConfig.symmetricRequireUserValiditySeconds = 10; + return secureConfig; + } + + /** + * Gets the default strong config with biometric support + * + * @param biometricSupport The biometric callback + * @return A secure config with strong NIAP settings built in and biometric support + */ + public static SecureConfig getStrongConfig(BiometricSupport biometricSupport) { + SecureConfig.Builder builder = new SecureConfig.Builder(); + builder.androidKeyStore = SecureConfig.ANDROID_KEYSTORE; + builder.androidCAStore = SecureConfig.ANDROID_CA_STORE; + builder.keystoreType = "PKCS12"; + + builder.asymmetricKeyPairAlgorithm = KeyProperties.KEY_ALGORITHM_RSA; + builder.asymmetricKeySize = 3072; + builder.teeGCIterations = 10; + //builder.asymmetricCipherTransformation = "RSA/ECB/PKCS1Padding"; + builder.asymmetricCipherTransformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + builder.asymmetricBlockModes = KeyProperties.BLOCK_MODE_ECB; + //builder.asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1; + builder.asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_OAEP; + builder.asymmetricKeyPurposes = KeyProperties.PURPOSE_DECRYPT; + builder.asymmetricSensitiveDataProtection = true; + builder.asymmetricRequireUserAuth = true; + builder.asymmetricRequireUserValiditySeconds = -1; + + builder.symmetricKeyAlgorithm = KeyProperties.KEY_ALGORITHM_AES; + builder.symmetricBlockModes = KeyProperties.BLOCK_MODE_GCM; + builder.symmetricPaddings = KeyProperties.ENCRYPTION_PADDING_NONE; + builder.symmetricKeySize = 256; + builder.symmetricGcmTagLength = 128; + builder.symmetricKeyPurposes = + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT; + builder.symmetricCipherTransformation = "AES/GCM/NoPadding"; + builder.symmetricSensitiveDataProtection = true; + builder.symmetricRequireUserAuth = true; + builder.symmetricRequireUserValiditySeconds = -1; + + builder.certPath = "X.509"; + builder.certPathValidator = "PKIX"; + builder.useStrongSSLCiphers = true; + builder.strongSSLCiphers = new String[]{ + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + }; + builder.clientCertAlgorithms = new String[]{"RSA"}; + builder.trustAnchorOptions = TrustAnchorOptions.USER_SYSTEM; + builder.biometricSupport = biometricSupport; + + return builder.build(); + } + + /** + * @return The android key store + */ + public String getAndroidKeyStore() { + return androidKeyStore; + } + + public void setAndroidKeyStore(String androidKeyStore) { + this.androidKeyStore = androidKeyStore; + } + + /** + * @return The Android CA store + */ + public String getAndroidCAStore() { + return androidCAStore; + } + + public void setAndroidCAStore(String androidCAStore) { + this.androidCAStore = androidCAStore; + } + + public String getKeystoreType() { + return keystoreType; + } + + public void setKeystoreType(String keystoreType) { + this.keystoreType = keystoreType; + } + + public String getAsymmetricKeyPairAlgorithm() { + return asymmetricKeyPairAlgorithm; + } + + public void setAsymmetricKeyPairAlgorithm(String asymmetricKeyPairAlgorithm) { + this.asymmetricKeyPairAlgorithm = asymmetricKeyPairAlgorithm; + } + + public int getAsymmetricKeySize() { + return asymmetricKeySize; + } + + public void setAsymmetricKeySize(int asymmetricKeySize) { + this.asymmetricKeySize = asymmetricKeySize; + } + + public String getAsymmetricCipherTransformation() { + return asymmetricCipherTransformation; + } + + public void setAsymmetricCipherTransformation(String asymmetricCipherTransformation) { + this.asymmetricCipherTransformation = asymmetricCipherTransformation; + } + + public String getAsymmetricBlockModes() { + return asymmetricBlockModes; + } + + public void setAsymmetricBlockModes(String asymmetricBlockModes) { + this.asymmetricBlockModes = asymmetricBlockModes; + } + + public String getAsymmetricPaddings() { + return asymmetricPaddings; + } + + public void setAsymmetricPaddings(String asymmetricPaddings) { + this.asymmetricPaddings = asymmetricPaddings; + } + + public int getAsymmetricKeyPurposes() { + return asymmetricKeyPurposes; + } + + public void setAsymmetricKeyPurposes(int asymmetricKeyPurposes) { + this.asymmetricKeyPurposes = asymmetricKeyPurposes; + } + + public boolean isAsymmetricSensitiveDataProtectionEnabled() { + return asymmetricSensitiveDataProtection; + } + + public void setAsymmetricSensitiveDataProtection(boolean asymmetricSensitiveDataProtection) { + this.asymmetricSensitiveDataProtection = asymmetricSensitiveDataProtection; + } + + public boolean isAsymmetricRequireUserAuthEnabled() { + return asymmetricRequireUserAuth && biometricSupport != null; + } + + public void setAsymmetricRequireUserAuth(boolean requireUserAuth) { + this.asymmetricRequireUserAuth = requireUserAuth; + } + + public int getAsymmetricRequireUserValiditySeconds() { + return this.asymmetricRequireUserValiditySeconds; + } + + public void setAsymmetricRequireUserValiditySeconds(int userValiditySeconds) { + this.asymmetricRequireUserValiditySeconds = userValiditySeconds; + } + + + public String getSymmetricKeyAlgorithm() { + return symmetricKeyAlgorithm; + } + + public void setSymmetricKeyAlgorithm(String symmetricKeyAlgorithm) { + this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; + } + + public String getSymmetricBlockModes() { + return symmetricBlockModes; + } + + public void setSymmetricBlockModes(String symmetricBlockModes) { + this.symmetricBlockModes = symmetricBlockModes; + } + + public String getSymmetricPaddings() { + return symmetricPaddings; + } + + public void setSymmetricPaddings(String symmetricPaddings) { + this.symmetricPaddings = symmetricPaddings; + } + + public int getSymmetricKeySize() { + return symmetricKeySize; + } + + public void setSymmetricKeySize(int symmetricKeySize) { + this.symmetricKeySize = symmetricKeySize; + } + + public int getSymmetricGcmTagLength() { + return symmetricGcmTagLength; + } + + public void setSymmetricGcmTagLength(int symmetricGcmTagLength) { + this.symmetricGcmTagLength = symmetricGcmTagLength; + } + + public int getSymmetricKeyPurposes() { + return symmetricKeyPurposes; + } + + public void setSymmetricKeyPurposes(int symmetricKeyPurposes) { + this.symmetricKeyPurposes = symmetricKeyPurposes; + } + + public String getSymmetricCipherTransformation() { + return symmetricCipherTransformation; + } + + public void setSymmetricCipherTransformation(String symmetricCipherTransformation) { + this.symmetricCipherTransformation = symmetricCipherTransformation; + } + + public boolean isSymmetricSensitiveDataProtectionEnabled() { + return symmetricSensitiveDataProtection; + } + + public void setSymmetricSensitiveDataProtection(boolean symmetricSensitiveDataProtection) { + this.symmetricSensitiveDataProtection = symmetricSensitiveDataProtection; + } + + public boolean isSymmetricRequireUserAuthEnabled() { + return symmetricRequireUserAuth; + } + + public void setSymmetricRequireUserAuth(boolean requireUserAuth) { + this.symmetricRequireUserAuth = requireUserAuth; + } + + public int getSymmetricRequireUserValiditySeconds() { + return this.symmetricRequireUserValiditySeconds; + } + + public void setSymmetricRequireUserValiditySeconds(int userValiditySeconds) { + this.symmetricRequireUserValiditySeconds = userValiditySeconds; + } + + public String getCertPath() { + return certPath; + } + + public void setCertPath(String certPath) { + this.certPath = certPath; + } + + public String getCertPathValidator() { + return certPathValidator; + } + + public void setCertPathValidator(String certPathValidator) { + this.certPathValidator = certPathValidator; + } + + public boolean isUseStrongSSLCiphersEnabled() { + return useStrongSSLCiphers; + } + + public void setUseStrongSSLCiphers(boolean useStrongSSLCiphers) { + this.useStrongSSLCiphers = useStrongSSLCiphers; + } + + public boolean isUseStrongSSLCiphers() { + return useStrongSSLCiphers; + } + + public String[] getStrongSSLCiphers() { + return strongSSLCiphers; + } + + public void setStrongSSLCiphers(String[] strongSSLCiphers) { + this.strongSSLCiphers = strongSSLCiphers; + } + + public String[] getClientCertAlgorithms() { + return clientCertAlgorithms; + } + + public void setClientCertAlgorithms(String[] clientCertAlgorithms) { + this.clientCertAlgorithms = clientCertAlgorithms; + } + + public TrustAnchorOptions getTrustAnchorOptions() { + return trustAnchorOptions; + } + + public void setTrustAnchorOptions(TrustAnchorOptions trustAnchorOptions) { + this.trustAnchorOptions = trustAnchorOptions; + } + + public BiometricSupport getBiometricSupport() { + return biometricSupport; + } + + public void setBiometricSupport(BiometricSupport biometricSupport) { + this.biometricSupport = biometricSupport; + } + + public void setDebugLoggingEnabled(boolean enabled) { + logging = enabled; + } + + public boolean isDebugLoggingEnabled() { + return logging; + } + + public int getTeeGCIterations() { + return teeGCIterations; + } + + public void setTeeGCIterations(int iterations) { + teeGCIterations = iterations; + } +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupport.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupport.java new file mode 100644 index 0000000..342c50c --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupport.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.biometric; + +import javax.crypto.Cipher; + +import com.android.certifications.niap.niapsec.crypto.SecureCipher; + +/** + * Interface to define various events and enums used by BiometricPrompt. + * + */ +public interface BiometricSupport { + + /** + * Statuses of biometric authentication + */ + public enum BiometricStatus { + SUCCESS(0), + FAILED(1), + CANCELLED(2); + + private final int type; + + BiometricStatus(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public static BiometricStatus fromId(int id) { + switch (id) { + case 0: + return SUCCESS; + case 1: + return FAILED; + case 2: + return CANCELLED; + } + return CANCELLED; + } + } + + void onAuthenticationSucceeded(); + + void onAuthenticationFailed(); + + void onMessage(String message); + + void authenticate(Cipher cipher, SecureCipher.SecureAuthCallback callback); + + void authenticateDeviceCredential(SecureCipher.SecureAuthCallback callback); + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupportImpl.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupportImpl.java new file mode 100644 index 0000000..a4ec0a2 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupportImpl.java @@ -0,0 +1,188 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.biometric; + +import android.content.Context; + + +import android.util.Log; + +import com.android.certifications.niap.niapsec.crypto.SecureCipher; + +import java.util.concurrent.Executor; + +import javax.crypto.Cipher; + +import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; + +/** + * Implementation of Biometric support that handles callbacks and showing the UI elements for + * key use authorization for the AndroidKeyStore. + */ +public abstract class BiometricSupportImpl extends BiometricPrompt.AuthenticationCallback + implements BiometricSupport { + + private static final String TAG = "BiometricSupportImpl"; + + private FragmentActivity activity; + private Context context; + private boolean useDeviceCredential = false; + private SecureCipher.SecureAuthCallback secureAuthCallback; + private static final String keyName = "biometric_key"; + private BiometricPrompt biometricPrompt; + private Executor executor; + + /** + * Create a Biometric support object with settings from the calling app + * + * @param activity The activity of the calling app + * @param context The context of the calling app + * @param useDeviceCredential true if testing on a device without biometrics and fingerprint + * support. false to test with the biometrics. + *

+ * NOTE: If useDeviceCredential is true, you must set your config + * to allow for a time-bound key approach in the android keystore. + * userValiditySeconds must be > 0 + */ + public BiometricSupportImpl(FragmentActivity activity, + Context context, + boolean useDeviceCredential) { + this.activity = activity; + this.context = context; + this.useDeviceCredential = useDeviceCredential; + this.executor = ContextCompat.getMainExecutor(activity); + this.biometricPrompt = new BiometricPrompt(activity, executor, this); + } + + @Override + public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + onAuthenticationSucceeded(); + Log.i(TAG, "SDP Unlock Succeeded, private key available for " + + "decryption through the AndroidKeyStore.\""); + try { + secureAuthCallback.authComplete(BiometricStatus.SUCCESS); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + Log.i(TAG, "SDP Unlock Error: " + errorCode + ": " + errString); + try { + onMessage(String.valueOf(errString)); + onAuthenticationFailed(); + secureAuthCallback.authComplete(BiometricStatus.FAILED); + } catch (SecurityException ex) { + ex.printStackTrace(); + } + } + + @Override + public void onAuthenticationFailed() { + super.onAuthenticationFailed(); + Log.i(TAG, "SDP Unlock Failed"); + try { + onAuthenticationFailed(); + secureAuthCallback.authComplete(BiometricStatus.FAILED); + } catch (SecurityException ex) { + ex.printStackTrace(); + } + } + + /** + * Show the auth dialog + * + * @param title The title of the prompt + * @param subtitle subtitle of the prompt + * @param description description of the prompt + * @param negativeButtonText cancel button text ( + * @param callback The callback to handle auth complete, failures, etc + * @param cryptoObject The crypto object to authenticate (Cipher, Signature, etc) + */ + public void showAuthDialog(String title, String subtitle, + String description, + String negativeButtonText, + SecureCipher.SecureAuthCallback callback, + BiometricPrompt.CryptoObject cryptoObject) { + this.secureAuthCallback = callback; + + //activity.runOnUiThread(() -> { + BiometricPrompt.PromptInfo.Builder promptInfoBuilder = + new BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setSubtitle(subtitle) + .setDescription(description); + if (useDeviceCredential) { + promptInfoBuilder.setDeviceCredentialAllowed(true); + } else { + promptInfoBuilder.setNegativeButtonText(negativeButtonText); + } + BiometricPrompt.PromptInfo promptInfo = promptInfoBuilder.build(); + try { + if (useDeviceCredential) { + Log.i(TAG, "Calling BiometricPrompt Authenticate for device credential."); + biometricPrompt.authenticate(promptInfo); + } else { + Log.i(TAG, "Calling BiometricPrompt Authenticate for biometrics."); + biometricPrompt.authenticate(promptInfo, cryptoObject); + } + } catch (IllegalArgumentException ex) { + Log.i(TAG, "Could not authenticate, make sure you have biometrics setup " + + "properly, if you set useDeviceCredential to true, you must not have " + + "biometrics available on the device"); + } + //}); + } + + /** + * Authenticate a cipher using Biometrics to unlock the AndroidKeyStore key + * + * @param cipher The cipher to authenticate + * @param callback The callback to call when auth is complete with the appropriate event + */ + public void authenticate(final Cipher cipher, SecureCipher.SecureAuthCallback callback) { + final BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(cipher); + showAuthDialog("Please Auth for key usage.", + "Key used for encrypting files", + "User authentication required to access key.", + "Cancel", + callback, + cryptoObject); + } + + /** + * Authenticate a device using Device Credential to unlock the time-bound AndroidKeyStore keys + *

+ * Use this when testing devices that do not have physical biometric or fingerprint hardware. + * + * @param callback The callback to call when auth is complete with the appropriate event + */ + public void authenticateDeviceCredential(SecureCipher.SecureAuthCallback callback) { + showAuthDialog("Please Auth for key usage.", + "Key used for encrypting files", + "User authentication required to access key.", + null, + callback, + null); + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TldConstants.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TldConstants.java new file mode 100644 index 0000000..10de486 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TldConstants.java @@ -0,0 +1,1567 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.config; + +import java.util.Arrays; +import java.util.List; + +/** + * A static list of known TLDs + */ +public class TldConstants { + public static final List VALID_TLDS = Arrays.asList( + "*.AAA", + "*.AARP", + "*.ABARTH", + "*.ABB", + "*.ABBOTT", + "*.ABBVIE", + "*.ABC", + "*.ABLE", + "*.ABOGADO", + "*.ABUDHABI", + "*.AC", + "*.ACADEMY", + "*.ACCENTURE", + "*.ACCOUNTANT", + "*.ACCOUNTANTS", + "*.ACO", + "*.ACTIVE", + "*.ACTOR", + "*.AD", + "*.ADAC", + "*.ADS", + "*.ADULT", + "*.AE", + "*.AEG", + "*.AERO", + "*.AETNA", + "*.AF", + "*.AFAMILYCOMPANY", + "*.AFL", + "*.AFRICA", + "*.AG", + "*.AGAKHAN", + "*.AGENCY", + "*.AI", + "*.AIG", + "*.AIGO", + "*.AIRBUS", + "*.AIRFORCE", + "*.AIRTEL", + "*.AKDN", + "*.AL", + "*.ALFAROMEO", + "*.ALIBABA", + "*.ALIPAY", + "*.ALLFINANZ", + "*.ALLSTATE", + "*.ALLY", + "*.ALSACE", + "*.ALSTOM", + "*.AM", + "*.AMERICANEXPRESS", + "*.AMERICANFAMILY", + "*.AMEX", + "*.AMFAM", + "*.AMICA", + "*.AMSTERDAM", + "*.ANALYTICS", + "*.ANDROID", + "*.ANQUAN", + "*.ANZ", + "*.AO", + "*.AOL", + "*.APARTMENTS", + "*.APP", + "*.APPLE", + "*.AQ", + "*.AQUARELLE", + "*.AR", + "*.ARAB", + "*.ARAMCO", + "*.ARCHI", + "*.ARMY", + "*.ARPA", + "*.ART", + "*.ARTE", + "*.AS", + "*.ASDA", + "*.ASIA", + "*.ASSOCIATES", + "*.AT", + "*.ATHLETA", + "*.ATTORNEY", + "*.AU", + "*.AUCTION", + "*.AUDI", + "*.AUDIBLE", + "*.AUDIO", + "*.AUSPOST", + "*.AUTHOR", + "*.AUTO", + "*.AUTOS", + "*.AVIANCA", + "*.AW", + "*.AWS", + "*.AX", + "*.AXA", + "*.AZ", + "*.AZURE", + "*.BA", + "*.BABY", + "*.BAIDU", + "*.BANAMEX", + "*.BANANAREPUBLIC", + "*.BAND", + "*.BANK", + "*.BAR", + "*.BARCELONA", + "*.BARCLAYCARD", + "*.BARCLAYS", + "*.BAREFOOT", + "*.BARGAINS", + "*.BASEBALL", + "*.BASKETBALL", + "*.BAUHAUS", + "*.BAYERN", + "*.BB", + "*.BBC", + "*.BBT", + "*.BBVA", + "*.BCG", + "*.BCN", + "*.BD", + "*.BE", + "*.BEATS", + "*.BEAUTY", + "*.BEER", + "*.BENTLEY", + "*.BERLIN", + "*.BEST", + "*.BESTBUY", + "*.BET", + "*.BF", + "*.BG", + "*.BH", + "*.BHARTI", + "*.BI", + "*.BIBLE", + "*.BID", + "*.BIKE", + "*.BING", + "*.BINGO", + "*.BIO", + "*.BIZ", + "*.BJ", + "*.BLACK", + "*.BLACKFRIDAY", + "*.BLANCO", + "*.BLOCKBUSTER", + "*.BLOG", + "*.BLOOMBERG", + "*.BLUE", + "*.BM", + "*.BMS", + "*.BMW", + "*.BN", + "*.BNL", + "*.BNPPARIBAS", + "*.BO", + "*.BOATS", + "*.BOEHRINGER", + "*.BOFA", + "*.BOM", + "*.BOND", + "*.BOO", + "*.BOOK", + "*.BOOKING", + "*.BOSCH", + "*.BOSTIK", + "*.BOSTON", + "*.BOT", + "*.BOUTIQUE", + "*.BOX", + "*.BR", + "*.BRADESCO", + "*.BRIDGESTONE", + "*.BROADWAY", + "*.BROKER", + "*.BROTHER", + "*.BRUSSELS", + "*.BS", + "*.BT", + "*.BUDAPEST", + "*.BUGATTI", + "*.BUILD", + "*.BUILDERS", + "*.BUSINESS", + "*.BUY", + "*.BUZZ", + "*.BV", + "*.BW", + "*.BY", + "*.BZ", + "*.BZH", + "*.CA", + "*.CAB", + "*.CAFE", + "*.CAL", + "*.CALL", + "*.CALVINKLEIN", + "*.CAM", + "*.CAMERA", + "*.CAMP", + "*.CANCERRESEARCH", + "*.CANON", + "*.CAPETOWN", + "*.CAPITAL", + "*.CAPITALONE", + "*.CAR", + "*.CARAVAN", + "*.CARDS", + "*.CARE", + "*.CAREER", + "*.CAREERS", + "*.CARS", + "*.CARTIER", + "*.CASA", + "*.CASE", + "*.CASEIH", + "*.CASH", + "*.CASINO", + "*.CAT", + "*.CATERING", + "*.CATHOLIC", + "*.CBA", + "*.CBN", + "*.CBRE", + "*.CBS", + "*.CC", + "*.CD", + "*.CEB", + "*.CENTER", + "*.CEO", + "*.CERN", + "*.CF", + "*.CFA", + "*.CFD", + "*.CG", + "*.CH", + "*.CHANEL", + "*.CHANNEL", + "*.CHARITY", + "*.CHASE", + "*.CHAT", + "*.CHEAP", + "*.CHINTAI", + "*.CHRISTMAS", + "*.CHROME", + "*.CHRYSLER", + "*.CHURCH", + "*.CI", + "*.CIPRIANI", + "*.CIRCLE", + "*.CISCO", + "*.CITADEL", + "*.CITI", + "*.CITIC", + "*.CITY", + "*.CITYEATS", + "*.CK", + "*.CL", + "*.CLAIMS", + "*.CLEANING", + "*.CLICK", + "*.CLINIC", + "*.CLINIQUE", + "*.CLOTHING", + "*.CLOUD", + "*.CLUB", + "*.CLUBMED", + "*.CM", + "*.CN", + "*.CO", + "*.COACH", + "*.CODES", + "*.COFFEE", + "*.COLLEGE", + "*.COLOGNE", + "*.COM", + "*.COMCAST", + "*.COMMBANK", + "*.COMMUNITY", + "*.COMPANY", + "*.COMPARE", + "*.COMPUTER", + "*.COMSEC", + "*.CONDOS", + "*.CONSTRUCTION", + "*.CONSULTING", + "*.CONTACT", + "*.CONTRACTORS", + "*.COOKING", + "*.COOKINGCHANNEL", + "*.COOL", + "*.COOP", + "*.CORSICA", + "*.COUNTRY", + "*.COUPON", + "*.COUPONS", + "*.COURSES", + "*.CR", + "*.CREDIT", + "*.CREDITCARD", + "*.CREDITUNION", + "*.CRICKET", + "*.CROWN", + "*.CRS", + "*.CRUISE", + "*.CRUISES", + "*.CSC", + "*.CU", + "*.CUISINELLA", + "*.CV", + "*.CW", + "*.CX", + "*.CY", + "*.CYMRU", + "*.CYOU", + "*.CZ", + "*.DABUR", + "*.DAD", + "*.DANCE", + "*.DATA", + "*.DATE", + "*.DATING", + "*.DATSUN", + "*.DAY", + "*.DCLK", + "*.DDS", + "*.DE", + "*.DEAL", + "*.DEALER", + "*.DEALS", + "*.DEGREE", + "*.DELIVERY", + "*.DELL", + "*.DELOITTE", + "*.DELTA", + "*.DEMOCRAT", + "*.DENTAL", + "*.DENTIST", + "*.DESI", + "*.DESIGN", + "*.DEV", + "*.DHL", + "*.DIAMONDS", + "*.DIET", + "*.DIGITAL", + "*.DIRECT", + "*.DIRECTORY", + "*.DISCOUNT", + "*.DISCOVER", + "*.DISH", + "*.DIY", + "*.DJ", + "*.DK", + "*.DM", + "*.DNP", + "*.DO", + "*.DOCS", + "*.DOCTOR", + "*.DODGE", + "*.DOG", + "*.DOHA", + "*.DOMAINS", + "*.DOT", + "*.DOWNLOAD", + "*.DRIVE", + "*.DTV", + "*.DUBAI", + "*.DUCK", + "*.DUNLOP", + "*.DUNS", + "*.DUPONT", + "*.DURBAN", + "*.DVAG", + "*.DVR", + "*.DZ", + "*.EARTH", + "*.EAT", + "*.EC", + "*.ECO", + "*.EDEKA", + "*.EDU", + "*.EDUCATION", + "*.EE", + "*.EG", + "*.EMAIL", + "*.EMERCK", + "*.ENERGY", + "*.ENGINEER", + "*.ENGINEERING", + "*.ENTERPRISES", + "*.EPOST", + "*.EPSON", + "*.EQUIPMENT", + "*.ER", + "*.ERICSSON", + "*.ERNI", + "*.ES", + "*.ESQ", + "*.ESTATE", + "*.ESURANCE", + "*.ET", + "*.ETISALAT", + "*.EU", + "*.EUROVISION", + "*.EUS", + "*.EVENTS", + "*.EVERBANK", + "*.EXCHANGE", + "*.EXPERT", + "*.EXPOSED", + "*.EXPRESS", + "*.EXTRASPACE", + "*.FAGE", + "*.FAIL", + "*.FAIRWINDS", + "*.FAITH", + "*.FAMILY", + "*.FAN", + "*.FANS", + "*.FARM", + "*.FARMERS", + "*.FASHION", + "*.FAST", + "*.FEDEX", + "*.FEEDBACK", + "*.FERRARI", + "*.FERRERO", + "*.FI", + "*.FIAT", + "*.FIDELITY", + "*.FIDO", + "*.FILM", + "*.FINAL", + "*.FINANCE", + "*.FINANCIAL", + "*.FIRE", + "*.FIRESTONE", + "*.FIRMDALE", + "*.FISH", + "*.FISHING", + "*.FIT", + "*.FITNESS", + "*.FJ", + "*.FK", + "*.FLICKR", + "*.FLIGHTS", + "*.FLIR", + "*.FLORIST", + "*.FLOWERS", + "*.FLY", + "*.FM", + "*.FO", + "*.FOO", + "*.FOOD", + "*.FOODNETWORK", + "*.FOOTBALL", + "*.FORD", + "*.FOREX", + "*.FORSALE", + "*.FORUM", + "*.FOUNDATION", + "*.FOX", + "*.FR", + "*.FREE", + "*.FRESENIUS", + "*.FRL", + "*.FROGANS", + "*.FRONTDOOR", + "*.FRONTIER", + "*.FTR", + "*.FUJITSU", + "*.FUJIXEROX", + "*.FUN", + "*.FUND", + "*.FURNITURE", + "*.FUTBOL", + "*.FYI", + "*.GA", + "*.GAL", + "*.GALLERY", + "*.GALLO", + "*.GALLUP", + "*.GAME", + "*.GAMES", + "*.GAP", + "*.GARDEN", + "*.GB", + "*.GBIZ", + "*.GD", + "*.GDN", + "*.GE", + "*.GEA", + "*.GENT", + "*.GENTING", + "*.GEORGE", + "*.GF", + "*.GG", + "*.GGEE", + "*.GH", + "*.GI", + "*.GIFT", + "*.GIFTS", + "*.GIVES", + "*.GIVING", + "*.GL", + "*.GLADE", + "*.GLASS", + "*.GLE", + "*.GLOBAL", + "*.GLOBO", + "*.GM", + "*.GMAIL", + "*.GMBH", + "*.GMO", + "*.GMX", + "*.GN", + "*.GODADDY", + "*.GOLD", + "*.GOLDPOINT", + "*.GOLF", + "*.GOO", + "*.GOODHANDS", + "*.GOODYEAR", + "*.GOOG", + "*.GOOGLE", + "*.GOP", + "*.GOT", + "*.GOV", + "*.GP", + "*.GQ", + "*.GR", + "*.GRAINGER", + "*.GRAPHICS", + "*.GRATIS", + "*.GREEN", + "*.GRIPE", + "*.GROCERY", + "*.GROUP", + "*.GS", + "*.GT", + "*.GU", + "*.GUARDIAN", + "*.GUCCI", + "*.GUGE", + "*.GUIDE", + "*.GUITARS", + "*.GURU", + "*.GW", + "*.GY", + "*.HAIR", + "*.HAMBURG", + "*.HANGOUT", + "*.HAUS", + "*.HBO", + "*.HDFC", + "*.HDFCBANK", + "*.HEALTH", + "*.HEALTHCARE", + "*.HELP", + "*.HELSINKI", + "*.HERE", + "*.HERMES", + "*.HGTV", + "*.HIPHOP", + "*.HISAMITSU", + "*.HITACHI", + "*.HIV", + "*.HK", + "*.HKT", + "*.HM", + "*.HN", + "*.HOCKEY", + "*.HOLDINGS", + "*.HOLIDAY", + "*.HOMEDEPOT", + "*.HOMEGOODS", + "*.HOMES", + "*.HOMESENSE", + "*.HONDA", + "*.HONEYWELL", + "*.HORSE", + "*.HOSPITAL", + "*.HOST", + "*.HOSTING", + "*.HOT", + "*.HOTELES", + "*.HOTELS", + "*.HOTMAIL", + "*.HOUSE", + "*.HOW", + "*.HR", + "*.HSBC", + "*.HT", + "*.HU", + "*.HUGHES", + "*.HYATT", + "*.HYUNDAI", + "*.IBM", + "*.ICBC", + "*.ICE", + "*.ICU", + "*.ID", + "*.IE", + "*.IEEE", + "*.IFM", + "*.IKANO", + "*.IL", + "*.IM", + "*.IMAMAT", + "*.IMDB", + "*.IMMO", + "*.IMMOBILIEN", + "*.IN", + "*.INC", + "*.INDUSTRIES", + "*.INFINITI", + "*.INFO", + "*.ING", + "*.INK", + "*.INSTITUTE", + "*.INSURANCE", + "*.INSURE", + "*.INT", + "*.INTEL", + "*.INTERNATIONAL", + "*.INTUIT", + "*.INVESTMENTS", + "*.IO", + "*.IPIRANGA", + "*.IQ", + "*.IR", + "*.IRISH", + "*.IS", + "*.ISELECT", + "*.ISMAILI", + "*.IST", + "*.ISTANBUL", + "*.IT", + "*.ITAU", + "*.ITV", + "*.IVECO", + "*.JAGUAR", + "*.JAVA", + "*.JCB", + "*.JCP", + "*.JE", + "*.JEEP", + "*.JETZT", + "*.JEWELRY", + "*.JIO", + "*.JLC", + "*.JLL", + "*.JM", + "*.JMP", + "*.JNJ", + "*.JO", + "*.JOBS", + "*.JOBURG", + "*.JOT", + "*.JOY", + "*.JP", + "*.JPMORGAN", + "*.JPRS", + "*.JUEGOS", + "*.JUNIPER", + "*.KAUFEN", + "*.KDDI", + "*.KE", + "*.KERRYHOTELS", + "*.KERRYLOGISTICS", + "*.KERRYPROPERTIES", + "*.KFH", + "*.KG", + "*.KH", + "*.KI", + "*.KIA", + "*.KIM", + "*.KINDER", + "*.KINDLE", + "*.KITCHEN", + "*.KIWI", + "*.KM", + "*.KN", + "*.KOELN", + "*.KOMATSU", + "*.KOSHER", + "*.KP", + "*.KPMG", + "*.KPN", + "*.KR", + "*.KRD", + "*.KRED", + "*.KUOKGROUP", + "*.KW", + "*.KY", + "*.KYOTO", + "*.KZ", + "*.LA", + "*.LACAIXA", + "*.LADBROKES", + "*.LAMBORGHINI", + "*.LAMER", + "*.LANCASTER", + "*.LANCIA", + "*.LANCOME", + "*.LAND", + "*.LANDROVER", + "*.LANXESS", + "*.LASALLE", + "*.LAT", + "*.LATINO", + "*.LATROBE", + "*.LAW", + "*.LAWYER", + "*.LB", + "*.LC", + "*.LDS", + "*.LEASE", + "*.LECLERC", + "*.LEFRAK", + "*.LEGAL", + "*.LEGO", + "*.LEXUS", + "*.LGBT", + "*.LI", + "*.LIAISON", + "*.LIDL", + "*.LIFE", + "*.LIFEINSURANCE", + "*.LIFESTYLE", + "*.LIGHTING", + "*.LIKE", + "*.LILLY", + "*.LIMITED", + "*.LIMO", + "*.LINCOLN", + "*.LINDE", + "*.LINK", + "*.LIPSY", + "*.LIVE", + "*.LIVING", + "*.LIXIL", + "*.LK", + "*.LLC", + "*.LOAN", + "*.LOANS", + "*.LOCKER", + "*.LOCUS", + "*.LOFT", + "*.LOL", + "*.LONDON", + "*.LOTTE", + "*.LOTTO", + "*.LOVE", + "*.LPL", + "*.LPLFINANCIAL", + "*.LR", + "*.LS", + "*.LT", + "*.LTD", + "*.LTDA", + "*.LU", + "*.LUNDBECK", + "*.LUPIN", + "*.LUXE", + "*.LUXURY", + "*.LV", + "*.LY", + "*.MA", + "*.MACYS", + "*.MADRID", + "*.MAIF", + "*.MAISON", + "*.MAKEUP", + "*.MAN", + "*.MANAGEMENT", + "*.MANGO", + "*.MAP", + "*.MARKET", + "*.MARKETING", + "*.MARKETS", + "*.MARRIOTT", + "*.MARSHALLS", + "*.MASERATI", + "*.MATTEL", + "*.MBA", + "*.MC", + "*.MCKINSEY", + "*.MD", + "*.ME", + "*.MED", + "*.MEDIA", + "*.MEET", + "*.MELBOURNE", + "*.MEME", + "*.MEMORIAL", + "*.MEN", + "*.MENU", + "*.MERCKMSD", + "*.METLIFE", + "*.MG", + "*.MH", + "*.MIAMI", + "*.MICROSOFT", + "*.MIL", + "*.MINI", + "*.MINT", + "*.MIT", + "*.MITSUBISHI", + "*.MK", + "*.ML", + "*.MLB", + "*.MLS", + "*.MM", + "*.MMA", + "*.MN", + "*.MO", + "*.MOBI", + "*.MOBILE", + "*.MOBILY", + "*.MODA", + "*.MOE", + "*.MOI", + "*.MOM", + "*.MONASH", + "*.MONEY", + "*.MONSTER", + "*.MOPAR", + "*.MORMON", + "*.MORTGAGE", + "*.MOSCOW", + "*.MOTO", + "*.MOTORCYCLES", + "*.MOV", + "*.MOVIE", + "*.MOVISTAR", + "*.MP", + "*.MQ", + "*.MR", + "*.MS", + "*.MSD", + "*.MT", + "*.MTN", + "*.MTR", + "*.MU", + "*.MUSEUM", + "*.MUTUAL", + "*.MV", + "*.MW", + "*.MX", + "*.MY", + "*.MZ", + "*.NA", + "*.NAB", + "*.NADEX", + "*.NAGOYA", + "*.NAME", + "*.NATIONWIDE", + "*.NATURA", + "*.NAVY", + "*.NBA", + "*.NC", + "*.NE", + "*.NEC", + "*.NET", + "*.NETBANK", + "*.NETFLIX", + "*.NETWORK", + "*.NEUSTAR", + "*.NEW", + "*.NEWHOLLAND", + "*.NEWS", + "*.NEXT", + "*.NEXTDIRECT", + "*.NEXUS", + "*.NF", + "*.NFL", + "*.NG", + "*.NGO", + "*.NHK", + "*.NI", + "*.NICO", + "*.NIKE", + "*.NIKON", + "*.NINJA", + "*.NISSAN", + "*.NISSAY", + "*.NL", + "*.NO", + "*.NOKIA", + "*.NORTHWESTERNMUTUAL", + "*.NORTON", + "*.NOW", + "*.NOWRUZ", + "*.NOWTV", + "*.NP", + "*.NR", + "*.NRA", + "*.NRW", + "*.NTT", + "*.NU", + "*.NYC", + "*.NZ", + "*.OBI", + "*.OBSERVER", + "*.OFF", + "*.OFFICE", + "*.OKINAWA", + "*.OLAYAN", + "*.OLAYANGROUP", + "*.OLDNAVY", + "*.OLLO", + "*.OM", + "*.OMEGA", + "*.ONE", + "*.ONG", + "*.ONL", + "*.ONLINE", + "*.ONYOURSIDE", + "*.OOO", + "*.OPEN", + "*.ORACLE", + "*.ORANGE", + "*.ORG", + "*.ORGANIC", + "*.ORIGINS", + "*.OSAKA", + "*.OTSUKA", + "*.OTT", + "*.OVH", + "*.PA", + "*.PAGE", + "*.PANASONIC", + "*.PANERAI", + "*.PARIS", + "*.PARS", + "*.PARTNERS", + "*.PARTS", + "*.PARTY", + "*.PASSAGENS", + "*.PAY", + "*.PCCW", + "*.PE", + "*.PET", + "*.PF", + "*.PFIZER", + "*.PG", + "*.PH", + "*.PHARMACY", + "*.PHD", + "*.PHILIPS", + "*.PHONE", + "*.PHOTO", + "*.PHOTOGRAPHY", + "*.PHOTOS", + "*.PHYSIO", + "*.PIAGET", + "*.PICS", + "*.PICTET", + "*.PICTURES", + "*.PID", + "*.PIN", + "*.PING", + "*.PINK", + "*.PIONEER", + "*.PIZZA", + "*.PK", + "*.PL", + "*.PLACE", + "*.PLAY", + "*.PLAYSTATION", + "*.PLUMBING", + "*.PLUS", + "*.PM", + "*.PN", + "*.PNC", + "*.POHL", + "*.POKER", + "*.POLITIE", + "*.PORN", + "*.POST", + "*.PR", + "*.PRAMERICA", + "*.PRAXI", + "*.PRESS", + "*.PRIME", + "*.PRO", + "*.PROD", + "*.PRODUCTIONS", + "*.PROF", + "*.PROGRESSIVE", + "*.PROMO", + "*.PROPERTIES", + "*.PROPERTY", + "*.PROTECTION", + "*.PRU", + "*.PRUDENTIAL", + "*.PS", + "*.PT", + "*.PUB", + "*.PW", + "*.PWC", + "*.PY", + "*.QA", + "*.QPON", + "*.QUEBEC", + "*.QUEST", + "*.QVC", + "*.RACING", + "*.RADIO", + "*.RAID", + "*.RE", + "*.READ", + "*.REALESTATE", + "*.REALTOR", + "*.REALTY", + "*.RECIPES", + "*.RED", + "*.REDSTONE", + "*.REDUMBRELLA", + "*.REHAB", + "*.REISE", + "*.REISEN", + "*.REIT", + "*.RELIANCE", + "*.REN", + "*.RENT", + "*.RENTALS", + "*.REPAIR", + "*.REPORT", + "*.REPUBLICAN", + "*.REST", + "*.RESTAURANT", + "*.REVIEW", + "*.REVIEWS", + "*.REXROTH", + "*.RICH", + "*.RICHARDLI", + "*.RICOH", + "*.RIGHTATHOME", + "*.RIL", + "*.RIO", + "*.RIP", + "*.RMIT", + "*.RO", + "*.ROCHER", + "*.ROCKS", + "*.RODEO", + "*.ROGERS", + "*.ROOM", + "*.RS", + "*.RSVP", + "*.RU", + "*.RUGBY", + "*.RUHR", + "*.RUN", + "*.RW", + "*.RWE", + "*.RYUKYU", + "*.SA", + "*.SAARLAND", + "*.SAFE", + "*.SAFETY", + "*.SAKURA", + "*.SALE", + "*.SALON", + "*.SAMSCLUB", + "*.SAMSUNG", + "*.SANDVIK", + "*.SANDVIKCOROMANT", + "*.SANOFI", + "*.SAP", + "*.SARL", + "*.SAS", + "*.SAVE", + "*.SAXO", + "*.SB", + "*.SBI", + "*.SBS", + "*.SC", + "*.SCA", + "*.SCB", + "*.SCHAEFFLER", + "*.SCHMIDT", + "*.SCHOLARSHIPS", + "*.SCHOOL", + "*.SCHULE", + "*.SCHWARZ", + "*.SCIENCE", + "*.SCJOHNSON", + "*.SCOR", + "*.SCOT", + "*.SD", + "*.SE", + "*.SEARCH", + "*.SEAT", + "*.SECURE", + "*.SECURITY", + "*.SEEK", + "*.SELECT", + "*.SENER", + "*.SERVICES", + "*.SES", + "*.SEVEN", + "*.SEW", + "*.SEX", + "*.SEXY", + "*.SFR", + "*.SG", + "*.SH", + "*.SHANGRILA", + "*.SHARP", + "*.SHAW", + "*.SHELL", + "*.SHIA", + "*.SHIKSHA", + "*.SHOES", + "*.SHOP", + "*.SHOPPING", + "*.SHOUJI", + "*.SHOW", + "*.SHOWTIME", + "*.SHRIRAM", + "*.SI", + "*.SILK", + "*.SINA", + "*.SINGLES", + "*.SITE", + "*.SJ", + "*.SK", + "*.SKI", + "*.SKIN", + "*.SKY", + "*.SKYPE", + "*.SL", + "*.SLING", + "*.SM", + "*.SMART", + "*.SMILE", + "*.SN", + "*.SNCF", + "*.SO", + "*.SOCCER", + "*.SOCIAL", + "*.SOFTBANK", + "*.SOFTWARE", + "*.SOHU", + "*.SOLAR", + "*.SOLUTIONS", + "*.SONG", + "*.SONY", + "*.SOY", + "*.SPACE", + "*.SPIEGEL", + "*.SPORT", + "*.SPOT", + "*.SPREADBETTING", + "*.SR", + "*.SRL", + "*.SRT", + "*.ST", + "*.STADA", + "*.STAPLES", + "*.STAR", + "*.STARHUB", + "*.STATEBANK", + "*.STATEFARM", + "*.STATOIL", + "*.STC", + "*.STCGROUP", + "*.STOCKHOLM", + "*.STORAGE", + "*.STORE", + "*.STREAM", + "*.STUDIO", + "*.STUDY", + "*.STYLE", + "*.SU", + "*.SUCKS", + "*.SUPPLIES", + "*.SUPPLY", + "*.SUPPORT", + "*.SURF", + "*.SURGERY", + "*.SUZUKI", + "*.SV", + "*.SWATCH", + "*.SWIFTCOVER", + "*.SWISS", + "*.SX", + "*.SY", + "*.SYDNEY", + "*.SYMANTEC", + "*.SYSTEMS", + "*.SZ", + "*.TAB", + "*.TAIPEI", + "*.TALK", + "*.TAOBAO", + "*.TARGET", + "*.TATAMOTORS", + "*.TATAR", + "*.TATTOO", + "*.TAX", + "*.TAXI", + "*.TC", + "*.TCI", + "*.TD", + "*.TDK", + "*.TEAM", + "*.TECH", + "*.TECHNOLOGY", + "*.TEL", + "*.TELEFONICA", + "*.TEMASEK", + "*.TENNIS", + "*.TEVA", + "*.TF", + "*.TG", + "*.TH", + "*.THD", + "*.THEATER", + "*.THEATRE", + "*.TIAA", + "*.TICKETS", + "*.TIENDA", + "*.TIFFANY", + "*.TIPS", + "*.TIRES", + "*.TIROL", + "*.TJ", + "*.TJMAXX", + "*.TJX", + "*.TK", + "*.TKMAXX", + "*.TL", + "*.TM", + "*.TMALL", + "*.TN", + "*.TO", + "*.TODAY", + "*.TOKYO", + "*.TOOLS", + "*.TOP", + "*.TORAY", + "*.TOSHIBA", + "*.TOTAL", + "*.TOURS", + "*.TOWN", + "*.TOYOTA", + "*.TOYS", + "*.TR", + "*.TRADE", + "*.TRADING", + "*.TRAINING", + "*.TRAVEL", + "*.TRAVELCHANNEL", + "*.TRAVELERS", + "*.TRAVELERSINSURANCE", + "*.TRUST", + "*.TRV", + "*.TT", + "*.TUBE", + "*.TUI", + "*.TUNES", + "*.TUSHU", + "*.TV", + "*.TVS", + "*.TW", + "*.TZ", + "*.UA", + "*.UBANK", + "*.UBS", + "*.UCONNECT", + "*.UG", + "*.UK", + "*.UNICOM", + "*.UNIVERSITY", + "*.UNO", + "*.UOL", + "*.UPS", + "*.US", + "*.UY", + "*.UZ", + "*.VA", + "*.VACATIONS", + "*.VANA", + "*.VANGUARD", + "*.VC", + "*.VE", + "*.VEGAS", + "*.VENTURES", + "*.VERISIGN", + "*.VERSICHERUNG", + "*.VET", + "*.VG", + "*.VI", + "*.VIAJES", + "*.VIDEO", + "*.VIG", + "*.VIKING", + "*.VILLAS", + "*.VIN", + "*.VIP", + "*.VIRGIN", + "*.VISA", + "*.VISION", + "*.VISTAPRINT", + "*.VIVA", + "*.VIVO", + "*.VLAANDEREN", + "*.VN", + "*.VODKA", + "*.VOLKSWAGEN", + "*.VOLVO", + "*.VOTE", + "*.VOTING", + "*.VOTO", + "*.VOYAGE", + "*.VU", + "*.VUELOS", + "*.WALES", + "*.WALMART", + "*.WALTER", + "*.WANG", + "*.WANGGOU", + "*.WARMAN", + "*.WATCH", + "*.WATCHES", + "*.WEATHER", + "*.WEATHERCHANNEL", + "*.WEBCAM", + "*.WEBER", + "*.WEBSITE", + "*.WED", + "*.WEDDING", + "*.WEIBO", + "*.WEIR", + "*.WF", + "*.WHOSWHO", + "*.WIEN", + "*.WIKI", + "*.WILLIAMHILL", + "*.WIN", + "*.WINDOWS", + "*.WINE", + "*.WINNERS", + "*.WME", + "*.WOLTERSKLUWER", + "*.WOODSIDE", + "*.WORK", + "*.WORKS", + "*.WORLD", + "*.WOW", + "*.WS", + "*.WTC", + "*.WTF", + "*.XBOX", + "*.XEROX", + "*.XFINITY", + "*.XIHUAN", + "*.XIN", + "*.XN--11B4C3D", + "*.XN--1CK2E1B", + "*.XN--1QQW23A", + "*.XN--2SCRJ9C", + "*.XN--30RR7Y", + "*.XN--3BST00M", + "*.XN--3DS443G", + "*.XN--3E0B707E", + "*.XN--3HCRJ9C", + "*.XN--3OQ18VL8PN36A", + "*.XN--3PXU8K", + "*.XN--42C2D9A", + "*.XN--45BR5CYL", + "*.XN--45BRJ9C", + "*.XN--45Q11C", + "*.XN--4GBRIM", + "*.XN--54B7FTA0CC", + "*.XN--55QW42G", + "*.XN--55QX5D", + "*.XN--5SU34J936BGSG", + "*.XN--5TZM5G", + "*.XN--6FRZ82G", + "*.XN--6QQ986B3XL", + "*.XN--80ADXHKS", + "*.XN--80AO21A", + "*.XN--80AQECDR1A", + "*.XN--80ASEHDB", + "*.XN--80ASWG", + "*.XN--8Y0A063A", + "*.XN--90A3AC", + "*.XN--90AE", + "*.XN--90AIS", + "*.XN--9DBQ2A", + "*.XN--9ET52U", + "*.XN--9KRT00A", + "*.XN--B4W605FERD", + "*.XN--BCK1B9A5DRE4C", + "*.XN--C1AVG", + "*.XN--C2BR7G", + "*.XN--CCK2B3B", + "*.XN--CG4BKI", + "*.XN--CLCHC0EA0B2G2A9GCD", + "*.XN--CZR694B", + "*.XN--CZRS0T", + "*.XN--CZRU2D", + "*.XN--D1ACJ3B", + "*.XN--D1ALF", + "*.XN--E1A4C", + "*.XN--ECKVDTC9D", + "*.XN--EFVY88H", + "*.XN--ESTV75G", + "*.XN--FCT429K", + "*.XN--FHBEI", + "*.XN--FIQ228C5HS", + "*.XN--FIQ64B", + "*.XN--FIQS8S", + "*.XN--FIQZ9S", + "*.XN--FJQ720A", + "*.XN--FLW351E", + "*.XN--FPCRJ9C3D", + "*.XN--FZC2C9E2C", + "*.XN--FZYS8D69UVGM", + "*.XN--G2XX48C", + "*.XN--GCKR3F0F", + "*.XN--GECRJ9C", + "*.XN--GK3AT1E", + "*.XN--H2BREG3EVE", + "*.XN--H2BRJ9C", + "*.XN--H2BRJ9C8C", + "*.XN--HXT814E", + "*.XN--I1B6B1A6A2E", + "*.XN--IMR513N", + "*.XN--IO0A7I", + "*.XN--J1AEF", + "*.XN--J1AMH", + "*.XN--J6W193G", + "*.XN--JLQ61U9W7B", + "*.XN--JVR189M", + "*.XN--KCRX77D1X4A", + "*.XN--KPRW13D", + "*.XN--KPRY57D", + "*.XN--KPU716F", + "*.XN--KPUT3I", + "*.XN--L1ACC", + "*.XN--LGBBAT1AD8J", + "*.XN--MGB9AWBF", + "*.XN--MGBA3A3EJT", + "*.XN--MGBA3A4F16A", + "*.XN--MGBA7C0BBN0A", + "*.XN--MGBAAKC7DVF", + "*.XN--MGBAAM7A8H", + "*.XN--MGBAB2BD", + "*.XN--MGBAI9AZGQP6J", + "*.XN--MGBAYH7GPA", + "*.XN--MGBB9FBPOB", + "*.XN--MGBBH1A", + "*.XN--MGBBH1A71E", + "*.XN--MGBC0A9AZCG", + "*.XN--MGBCA7DZDO", + "*.XN--MGBERP4A5D4AR", + "*.XN--MGBGU82A", + "*.XN--MGBI4ECEXP", + "*.XN--MGBPL2FH", + "*.XN--MGBT3DHD", + "*.XN--MGBTX2B", + "*.XN--MGBX4CD0AB", + "*.XN--MIX891F", + "*.XN--MK1BU44C", + "*.XN--MXTQ1M", + "*.XN--NGBC5AZD", + "*.XN--NGBE9E0A", + "*.XN--NGBRX", + "*.XN--NODE", + "*.XN--NQV7F", + "*.XN--NQV7FS00EMA", + "*.XN--NYQY26A", + "*.XN--O3CW4H", + "*.XN--OGBPF8FL", + "*.XN--OTU796D", + "*.XN--P1ACF", + "*.XN--P1AI", + "*.XN--PBT977C", + "*.XN--PGBS0DH", + "*.XN--PSSY2U", + "*.XN--Q9JYB4C", + "*.XN--QCKA1PMC", + "*.XN--QXAM", + "*.XN--RHQV96G", + "*.XN--ROVU88B", + "*.XN--RVC1E0AM3E", + "*.XN--S9BRJ9C", + "*.XN--SES554G", + "*.XN--T60B56A", + "*.XN--TCKWE", + "*.XN--TIQ49XQYJ", + "*.XN--UNUP4Y", + "*.XN--VERMGENSBERATER-CTB", + "*.XN--VERMGENSBERATUNG-PWB", + "*.XN--VHQUV", + "*.XN--VUQ861B", + "*.XN--W4R85EL8FHU5DNRA", + "*.XN--W4RS40L", + "*.XN--WGBH1C", + "*.XN--WGBL6A", + "*.XN--XHQ521B", + "*.XN--XKC2AL3HYE2A", + "*.XN--XKC2DL3A5EE0H", + "*.XN--Y9A3AQ", + "*.XN--YFRO4I67O", + "*.XN--YGBI2AMMX", + "*.XN--ZFR164B", + "*.XXX", + "*.XYZ", + "*.YACHTS", + "*.YAHOO", + "*.YAMAXUN", + "*.YANDEX", + "*.YE", + "*.YODOBASHI", + "*.YOGA", + "*.YOKOHAMA", + "*.YOU", + "*.YOUTUBE", + "*.YT", + "*.YUN", + "*.ZA", + "*.ZAPPOS", + "*.ZARA", + "*.ZERO", + "*.ZIP", + "*.ZIPPO", + "*.ZM", + "*.ZONE", + "*.ZUERICH", + "*.ZW" + ); +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TrustAnchorOptions.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TrustAnchorOptions.java new file mode 100644 index 0000000..6b2b8b8 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TrustAnchorOptions.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.config; + +/** + * Trust anchor options, for specifying which type of trust anchors to use + */ +public enum TrustAnchorOptions { + USER_SYSTEM(0), + SYSTEM_ONLY(1), + USER_ONLY(2), + LIMITED_SYSTEM(3); + + private final int type; + + TrustAnchorOptions(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public static TrustAnchorOptions fromId(int id) { + switch (id) { + case 0: + return USER_SYSTEM; + case 1: + return SYSTEM_ONLY; + case 2: + return USER_ONLY; + case 3: + return LIMITED_SYSTEM; + } + return USER_SYSTEM; + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/context/SecureContextCompat.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/context/SecureContextCompat.java new file mode 100644 index 0000000..f9bb6d4 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/context/SecureContextCompat.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.context; + +import android.annotation.TargetApi; +import android.app.KeyguardManager; +import android.content.Context; +import android.os.Build; + +import androidx.annotation.NonNull; + +import com.android.certifications.niap.niapsec.SecureConfig; +import com.android.certifications.niap.niapsec.crypto.AuthenticatedFileCipher; +import com.android.certifications.niap.niapsec.crypto.FileCipher; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.concurrent.Executor; + +/** + * An extended context wrapper to handle passing the Context around and settings needed by NIAPSEC + */ +public class SecureContextCompat { + + private static final String TAG = "SecureContextCompat"; + + private Context mContext; + private SecureConfig mSecureConfig; + + private static final String DEFAULT_FILE_ENCRYPTION_KEY = "default_encryption_key"; + + /** + * Listener interface for encyrpted file input, reads may require authorization + */ + public interface EncryptedFileInputStreamListener { + void onEncryptedFileInput(FileInputStream inputStream); + } + + /** + * Builds a SecureContext with the provided context with custom settings + * + * @param context The context of the calling app + * @param secureConfig The configuration + */ + public SecureContextCompat(@NonNull Context context, @NonNull SecureConfig secureConfig) { + mContext = context; + mSecureConfig = secureConfig; + } + + /** + * Open an encrypted private file associated with this Context's application package for + * reading. + * + * @param name The name of the file to open; can not contain path separators. + * @throws IOException + */ + public void openEncryptedFileInput(@NonNull String name, + @NonNull Executor executor, + @NonNull boolean keyLocked, + @NonNull EncryptedFileInputStreamListener listener) + throws IOException { + if(keyLocked) { + new FileCipher(name, mContext.openFileInput(name), mSecureConfig, executor, listener); + } else { + new AuthenticatedFileCipher(name, mContext.openFileInput(name), mSecureConfig, + listener); + } + } + + /** + * Open a private encrypted file associated with this Context's application package for writing. + * Creates the file if it doesn't already exist. + *

+ * The written file will be encrypted with the default keyPairAlias. + * + * @param name The name of the file to open; can not contain path separators. + * @param mode Operating mode. + * @return The resulting {@link FileOutputStream}. + * @throws IOException + */ + public FileOutputStream openEncryptedFileOutput(@NonNull String name, @NonNull int mode, + boolean keyLocked) + throws IOException { + return openEncryptedFileOutput(name, mode, DEFAULT_FILE_ENCRYPTION_KEY, keyLocked); + } + + /** + * Open a private encrypted file associated with this Context's application package for writing. + * Creates the file if it doesn't already exist. + *

+ * The written file will be encrypted with the specified keyPairAlias. + * + * @param name The name of the file to open; can not contain path separators. + * @param mode Operating mode. + * @param keyPairAlias The alias of the KeyPair used for encryption, the KeyPair will be + * created if it does not exist. + * @return The resulting {@link FileOutputStream}. + * @throws IOException + */ + public FileOutputStream openEncryptedFileOutput(@NonNull String name, + @NonNull int mode, + @NonNull String keyPairAlias, + boolean keyLocked) + throws IOException { + if (keyLocked) { + FileCipher fileCipher = new FileCipher(keyPairAlias, + mContext.openFileOutput(name, mode), + mSecureConfig); + return fileCipher.getFileOutputStream(); + } else { + AuthenticatedFileCipher authenticatedFileCipher = new AuthenticatedFileCipher( + keyPairAlias, + mContext.openFileOutput(name, mode), + mSecureConfig); + return authenticatedFileCipher.getFileOutputStream(); + } + } + + + /** + * Checks if the device is locked + * + * @return true if the device is locked, false otherwise + */ + public boolean deviceLocked() { + KeyguardManager keyGuardManager = + (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); + return keyGuardManager.isDeviceLocked(); + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/AuthenticatedFileCipher.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/AuthenticatedFileCipher.java new file mode 100644 index 0000000..90bb82e --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/AuthenticatedFileCipher.java @@ -0,0 +1,314 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.crypto; + +import android.util.Base64; +import android.util.Log; +import android.util.Pair; + +import com.android.certifications.niap.niapsec.SecureConfig; +import com.android.certifications.niap.niapsec.context.SecureContextCompat; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.concurrent.Executor; + +import androidx.annotation.NonNull; + +/** + * Combines Cipher and File to allow for easy to write and read encrypted files. This assumes that + * the AndroidKeyStore has been unlocked and keys are available for use. + *

+ * As per NIAP, this class encrypts the file contents with an ephemeral symmetric data encryption + * key and encodes the necessary information for decryption. The ephemeral key is encrypted with an + * asymmetric key encryption key. + */ +public class AuthenticatedFileCipher { + + private String mFileName; + private String mKeyPairAlias; + private FileInputStream mFileInputStream; + private FileOutputStream mFileOutputStream; + + private SecureContextCompat.EncryptedFileInputStreamListener mListener; + + private SecureConfig mSecureConfig; + + /** + * Instantiates a FileCipher to handle read. Encryption and decryption is + * handled internally. + * + * @param fileName The file path of the file to open + * @param fileInputStream The input stream of the File to read + * @param secureConfig The secure configuration used, which specifies algorithms, key sizes, etc + * @throws IOException When there is a file read issue + */ + public AuthenticatedFileCipher(String fileName, FileInputStream fileInputStream, + SecureConfig secureConfig, + SecureContextCompat.EncryptedFileInputStreamListener listener) + throws IOException { + mFileName = fileName; + mFileInputStream = fileInputStream; + mSecureConfig = secureConfig; + EncryptedFileInputStream encryptedFileInputStream = + new EncryptedFileInputStream(mFileInputStream); + setEncryptedFileInputStreamListener(listener); + encryptedFileInputStream.decrypt(listener); + } + + /** + * Instantiates a FileCipher to handle read. Encryption and decryption is + * handled internally. + * + * @param keyPairAlias The RSA key pair alias of the key stored in the AndroidKeyStore + * @param fileOutputStream The output stream for writing + * @param secureConfig The secure configuration used, which specifies algorithms, + * key sizes, etc + */ + public AuthenticatedFileCipher(String keyPairAlias, FileOutputStream fileOutputStream, + SecureConfig secureConfig) { + mKeyPairAlias = keyPairAlias; + mFileOutputStream = new EncryptedFileOutputStream( + mFileName, + mKeyPairAlias, + fileOutputStream); + mSecureConfig = secureConfig; + } + + /** + * Set the executor and listener for keystore backed authenticated requests + * + * @param listener The listener which is called after a biometric authorization + */ + public void setEncryptedFileInputStreamListener( + SecureContextCompat.EncryptedFileInputStreamListener listener) { + mListener = listener; + } + + /** + * @return The file output stream for writing + */ + public FileOutputStream getFileOutputStream() { + return mFileOutputStream; + } + + /** + * @return The input stream for reading + */ + public FileInputStream getFileInputStream() { + return mFileInputStream; + } + + /** + * Internal class adding encryption to writes + */ + class EncryptedFileOutputStream extends FileOutputStream { + private static final String WRITE_NOT_SUPPORTED = "For encrypted files, you must write " + + "all data simultaneously. Call #write(byte[])."; + + private static final String TAG = "EncryptedFOS"; + + private FileOutputStream fileOutputStream; + private String keyPairAlias; + + EncryptedFileOutputStream(String name, + String keyPairAlias, + FileOutputStream fileOutputStream) { + super(new FileDescriptor()); + this.keyPairAlias = keyPairAlias; + this.fileOutputStream = fileOutputStream; + } + + private String getAsymKeyPairAlias() { + return this.keyPairAlias; + } + + @Override + public void write(@NonNull byte[] b) { + SecureKeyStore secureKeyStore = SecureKeyStore.getDefault(mSecureConfig); + if (!secureKeyStore.keyExists(getAsymKeyPairAlias())) { + SecureKeyGenerator keyGenerator = SecureKeyGenerator.getInstance(mSecureConfig); + keyGenerator.generateAsymmetricKeyPair(getAsymKeyPairAlias()); + } + SecureKeyGenerator secureKeyGenerator = SecureKeyGenerator.getInstance(mSecureConfig); + EphemeralSecretKey secretKey = secureKeyGenerator.generateEphemeralDataKey(); + if (mSecureConfig.isDebugLoggingEnabled()) { + Log.i("AuthenticatedFileCipher", "Calling: " + SecureRandom.class.getSimpleName() + + " EphemeralSecretKey: Base64:\n" + + Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT)); + } + SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig); + Pair encryptedData = secureCipher.encryptEphemeralData(secretKey, b, + keyPairAlias); + byte[] encryptedEphemeralKey = secureCipher.encryptSensitiveDataAsymmetric( + getAsymKeyPairAlias(), + secretKey.getEncoded()); + secretKey.destroy(); + byte[] encodedData = secureCipher.encodeEphemeralData( + getAsymKeyPairAlias().getBytes(), + encryptedEphemeralKey, + encryptedData.first, + encryptedData.second); + try { + fileOutputStream.write(encodedData); + } catch (IOException e) { + Log.e(TAG, "Failed to write secure file."); + e.printStackTrace(); + } + } + + @Override + public void write(int b) throws IOException { + throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED); + } + + @Override + public void write(@NonNull byte[] b, int off, int len) throws IOException { + throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED); + } + + @Override + public void close() throws IOException { + fileOutputStream.close(); + } + + @NonNull + @Override + public FileChannel getChannel() { + throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED); + } + + @Override + protected void finalize() throws IOException { + super.finalize(); + } + + @Override + public void flush() throws IOException { + fileOutputStream.flush(); + } + } + + /** + * Internal class adding encryption to reads + */ + class EncryptedFileInputStream extends FileInputStream { + private static final String READ_NOT_SUPPORTED = "For encrypted files, you must read all " + + "data simultaneously. Call #read(byte[])."; + + // Was 25 characters, truncating to fix compile error + private static final String TAG = "EncryptedFIS"; + + private FileInputStream fileInputStream; + private byte[] decryptedData; + private int readStatus = 0; + + EncryptedFileInputStream(FileInputStream fileInputStream) { + super(new FileDescriptor()); + this.fileInputStream = fileInputStream; + } + + @Override + public int read() throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + void decrypt(SecureContextCompat.EncryptedFileInputStreamListener listener) + throws IOException { + if (this.decryptedData == null) { + try { + byte[] encodedData = new byte[fileInputStream.available()]; + readStatus = fileInputStream.read(encodedData); + SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig); + this.decryptedData = secureCipher.decryptEncodedData(encodedData); + listener.onEncryptedFileInput(this); + } catch (IOException ex) { + throw ex; + } + } + } + + private void destroyCache() { + if (decryptedData != null) { + Arrays.fill(decryptedData, (byte) 0); + decryptedData = null; + } + } + + @Override + public int read(@NonNull byte[] b) { + System.arraycopy(decryptedData, 0, b, 0, decryptedData.length); + return readStatus; + } + + // TODO, implement this + @Override + public int read(@NonNull byte[] b, int off, int len) throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + // TODO, implement this + @Override + public long skip(long n) throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + public int available() { + return decryptedData.length; + } + + @Override + public void close() throws IOException { + destroyCache(); + fileInputStream.close(); + } + + @Override + public FileChannel getChannel() { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + protected void finalize() throws IOException { + destroyCache(); + super.finalize(); + } + + @Override + public synchronized void mark(int readlimit) { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + public synchronized void reset() throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + public boolean markSupported() { + return false; + } + + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/EphemeralSecretKey.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/EphemeralSecretKey.java new file mode 100644 index 0000000..7ae074e --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/EphemeralSecretKey.java @@ -0,0 +1,329 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.crypto; + +import android.security.keystore.KeyProperties; +import android.util.Log; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.KeySpec; +import java.security.spec.MGF1ParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; + +/** + * An implementation of SecretKey and KeySpec that has the ability to zero out the key material + * to make it harder to access the contents in a memory dump. + */ +public class EphemeralSecretKey implements KeySpec, SecretKey { + + private static final long serialVersionUID = 7177238317307289223L; + private static final String TAG = "EphemeralSecretKey"; + + private byte[] key; + + private String algorithm; + + private boolean keyDestroyed; + + private SecureConfig secureConfig; + + /** + * Instantiate a secret key from an existing key + * + * @param key The a generated key created from {@link SecureKeyGenerator} + */ + public EphemeralSecretKey(byte[] key, SecureConfig secureConfig) { + this(key, KeyProperties.KEY_ALGORITHM_AES, secureConfig); + } + + /** + * Constructs a secret key from the given byte array. + * + *

This constructor does not check if the given bytes indeed specify a + * secret key of the specified algorithm. For example, if the algorithm is + * DES, this constructor does not check if key is 8 bytes + * long, and also does not check for weak or semi-weak keys. + * In order for those checks to be performed, an algorithm-specific + * key specification class (in this case: + * {@link DESKeySpec DESKeySpec}) + * should be used. + * + * @param key the key material of the secret key. The contents of + * the array are copied to protect against subsequent modification. + * @param algorithm the name of the secret-key algorithm to be associated + * with the given key material. + * See Appendix A in the + * Java Cryptography Architecture Reference Guide + * for information about standard algorithm names. + * @throws IllegalArgumentException if algorithm + * is null or key is null or empty. + */ + public EphemeralSecretKey(byte[] key, String algorithm, SecureConfig secureConfig) { + if (key == null || algorithm == null) { + throw new IllegalArgumentException("Missing argument"); + } + if (key.length == 0) { + throw new IllegalArgumentException("Empty key"); + } + this.key = key; + this.algorithm = algorithm; + this.keyDestroyed = false; + this.secureConfig = secureConfig; + } + + /** + * Constructs a secret key from the given byte array, using the first + * len bytes of key, starting at + * offset inclusive. + * + *

The bytes that constitute the secret key are + * those between key[offset] and + * key[offset+len-1] inclusive. + * + *

This constructor does not check if the given bytes indeed specify a + * secret key of the specified algorithm. For example, if the algorithm is + * DES, this constructor does not check if key is 8 bytes + * long, and also does not check for weak or semi-weak keys. + * In order for those checks to be performed, an algorithm-specific key + * specification class (in this case: + * {@link DESKeySpec DESKeySpec}) + * must be used. + * + * @param key the key material of the secret key. The first + * len bytes of the array beginning at + * offset inclusive are copied to protect + * against subsequent modification. + * @param offset the offset in key where the key material + * starts. + * @param len the length of the key material. + * @param algorithm the name of the secret-key algorithm to be associated + * with the given key material. + * See Appendix A in the + * Java Cryptography Architecture Reference Guide + * for information about standard algorithm names. + * @throws IllegalArgumentException if algorithm + * is null or key is null, empty, or too short, + * i.e. {@code key.length-offsetoffset or len index bytes outside the + * key. + */ + public EphemeralSecretKey(byte[] key, int offset, int len, String algorithm) { + if (key == null || algorithm == null) { + throw new IllegalArgumentException("Missing argument"); + } + if (key.length == 0) { + throw new IllegalArgumentException("Empty key"); + } + if (key.length - offset < len) { + throw new IllegalArgumentException + ("Invalid offset/length combination"); + } + if (len < 0) { + throw new ArrayIndexOutOfBoundsException("len is negative"); + } + this.key = new byte[len]; + System.arraycopy(key, offset, this.key, 0, len); + this.algorithm = algorithm; + this.keyDestroyed = false; + } + + /** + * Returns the name of the algorithm associated with this secret key. + * + * @return the secret key algorithm. + */ + public String getAlgorithm() { + return this.algorithm; + } + + /** + * Returns the name of the encoding format for this secret key. + * + * @return the string "RAW". + */ + public String getFormat() { + return "RAW"; + } + + /** + * Returns the key material of this secret key. + * + * @return the key material. Returns a new array + * each time this method is called. + */ + public byte[] getEncoded() { + return this.key; + } + + /** + * Calculates a hash code value for the object. + * Objects that are equal will also have the same hashcode. + */ + public int hashCode() { + int retval = 0; + for (int i = 1; i < this.key.length; i++) { + retval += this.key[i] * i; + } + if (this.algorithm.equalsIgnoreCase("TripleDES")) + return (retval ^= "desede".hashCode()); + else + return (retval ^= + this.algorithm.toLowerCase(Locale.ENGLISH).hashCode()); + } + + /** + * Tests for equality between the specified object and this + * object. Two SecretKeySpec objects are considered equal if + * they are both SecretKey instances which have the + * same case-insensitive algorithm name and key encoding. + * + * @param obj the object to test for equality with this object. + * @return true if the objects are considered equal, false if + * obj is null or otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (!(obj instanceof SecretKey)) + return false; + + String thatAlg = ((SecretKey) obj).getAlgorithm(); + if (!(thatAlg.equalsIgnoreCase(this.algorithm))) { + if ((!(thatAlg.equalsIgnoreCase("DESede")) + || !(this.algorithm.equalsIgnoreCase("TripleDES"))) + && (!(thatAlg.equalsIgnoreCase("TripleDES")) + || !(this.algorithm.equalsIgnoreCase("DESede")))) + return false; + } + + byte[] thatKey = ((SecretKey) obj).getEncoded(); + + return MessageDigest.isEqual(this.key, thatKey); + } + + /** + * Overriding destroy to actually destroy the key, the default SecretKey implementations don't + * actually do antying in destroy to remove the key from memory. + */ + @Override + public void destroy() { + if (!keyDestroyed) { + Arrays.fill(key, (byte) 0); + this.key = null; + keyDestroyed = true; + System.gc(); + } + } + + + /** + * Clears the current key stored in this object. + * + * @param cipher The cipher used when operating with the key + * @param opmode The opmode used for Cipher creation + */ + public void destroyCipherKey(Cipher cipher, int opmode, String keyPairAlias) { + try { + byte[] blankKey = new byte[secureConfig.getSymmetricKeySize() / 8]; + byte[] iv = new byte[SecureConfig.AES_IV_SIZE_BYTES]; + Arrays.fill(blankKey, (byte) 0); + Arrays.fill(iv, (byte) 0); + EphemeralSecretKey blankSecretKey = new EphemeralSecretKey( + blankKey, + secureConfig.getSymmetricKeyAlgorithm(), + secureConfig); + cipher.init(opmode, blankSecretKey, new GCMParameterSpec( + secureConfig.getSymmetricGcmTagLength(), + iv)); + teeGC(keyPairAlias); + System.gc(); + } catch (GeneralSecurityException e) { + throw new SecurityException("Could not destroy key."); + } + } + + @Override + public boolean isDestroyed() { + return keyDestroyed; + } + + private void teeGC(String keyPairAlias) { + try { + int iterations = secureConfig.getTeeGCIterations(); + List arbitraryEncryptedAESKeys = new ArrayList<>(); + SecureRandom secureRandom = SecureRandom.getInstanceStrong(); + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + PublicKey publicKey = keyStore.getCertificate(keyPairAlias).getPublicKey(); + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + for (int i = 0; i < iterations; i++) { + byte[] key = new byte[256 / 8]; + secureRandom.nextBytes(key); + cipher.init(Cipher.ENCRYPT_MODE, + publicKey, + new OAEPParameterSpec("SHA-256", + "MGF1", + new MGF1ParameterSpec("SHA-1"), + PSource.PSpecified.DEFAULT)); + byte[] bytes = cipher.doFinal(key); + if (bytes != null) { + arbitraryEncryptedAESKeys.add(bytes); + } + } + PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyPairAlias, null); + for (int i = 0; i < iterations; i++) { + byte[] aesKey = arbitraryEncryptedAESKeys.get(i); + cipher.init(Cipher.DECRYPT_MODE, + privateKey, + new OAEPParameterSpec( + "SHA-256", + "MGF1", + new MGF1ParameterSpec("SHA-1"), + PSource.PSpecified.DEFAULT)); + byte[] rawKey = cipher.doFinal(aesKey); + } + arbitraryEncryptedAESKeys.clear(); + } catch (Exception ex) { + + } + System.gc(); + System.runFinalization(); + } + +} + diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/FileCipher.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/FileCipher.java new file mode 100644 index 0000000..89f090a --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/FileCipher.java @@ -0,0 +1,325 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.crypto; + +import android.util.Base64; +import android.util.Log; +import android.util.Pair; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.concurrent.Executor; + +import androidx.annotation.NonNull; +import com.android.certifications.niap.niapsec.SecureConfig; +import com.android.certifications.niap.niapsec.context.SecureContextCompat; + +/** + * Combines Cipher and File to allow for easy to write and read encrypted files. + * + * As per NIAP, this class encrypts the file contents with an ephemeral symmetric data encryption + * key and encodes the necessary information for decryption. The ephemeral key is encrypted with an + * asymmetric key encryption key. + */ +public class FileCipher { + + private String mFileName; + private String mKeyPairAlias; + private FileInputStream mFileInputStream; + private FileOutputStream mFileOutputStream; + + private Executor mExecutor; + private SecureContextCompat.EncryptedFileInputStreamListener mListener; + + private SecureConfig mSecureConfig; + + /** + * Instantiates a FileCipher to handle read. Encryption and decryption is + * handled internally. + * + * @param fileName The file path of the file to open + * @param fileInputStream The input stream of the File to read + * @param secureConfig The secure configuration used, which specifies algorithms, key sizes, etc + * @param executor An executor + * @param listener The listener for callbacks, this is necessary for key auth using + * BiometricPrompt + * @throws IOException When there is a file read issue + */ + public FileCipher(String fileName, FileInputStream fileInputStream, + SecureConfig secureConfig, Executor executor, + SecureContextCompat.EncryptedFileInputStreamListener listener) + throws IOException { + mFileName = fileName; + mFileInputStream = fileInputStream; + mSecureConfig = secureConfig; + EncryptedFileInputStream encryptedFileInputStream = + new EncryptedFileInputStream(mFileInputStream); + setEncryptedFileInputStreamListener(executor, listener); + encryptedFileInputStream.decrypt(listener); + } + + /** + * Instantiates a FileCipher to handle read. Encryption and decryption is + * handled internally. + * + * @param keyPairAlias The RSA key pair alias of the key stored in the AndroidKeyStore + * @param fileOutputStream The output stream for writing + * @param secureConfig The secure configuration used, which specifies algorithms, key sizes, etc + */ + public FileCipher(String keyPairAlias, FileOutputStream fileOutputStream, + SecureConfig secureConfig) { + mKeyPairAlias = keyPairAlias; + mFileOutputStream = new EncryptedFileOutputStream( + mFileName, + mKeyPairAlias, + fileOutputStream); + mSecureConfig = secureConfig; + } + + /** + * Set the executor and listener for keystore backed authenticated requests + * + * @param executor The executor for the callback + * @param listener The listener which is called after a biometric authorization + */ + public void setEncryptedFileInputStreamListener(@NonNull Executor executor, + @NonNull + SecureContextCompat + .EncryptedFileInputStreamListener + listener) { + mExecutor = executor; + mListener = listener; + } + + /** + * @return The file output stream for writing + */ + public FileOutputStream getFileOutputStream() { + return mFileOutputStream; + } + + /** + * @return The input stream for reading + */ + public FileInputStream getFileInputStream() { + return mFileInputStream; + } + + /** + * Internal class adding encryption to writes + */ + class EncryptedFileOutputStream extends FileOutputStream { + private static final String WRITE_NOT_SUPPORTED = "For encrypted files, you must write " + + "all data simultaneously. Call #write(byte[])."; + + private static final String TAG = "EncryptedFOS"; + + private FileOutputStream fileOutputStream; + private String keyPairAlias; + + EncryptedFileOutputStream(String name, + String keyPairAlias, + FileOutputStream fileOutputStream) { + super(new FileDescriptor()); + this.keyPairAlias = keyPairAlias; + this.fileOutputStream = fileOutputStream; + } + + private String getAsymKeyPairAlias() { + return this.keyPairAlias; + } + + @Override + public void write(@NonNull byte[] b) { + SecureKeyStore secureKeyStore = SecureKeyStore.getDefault(mSecureConfig); + if (!secureKeyStore.keyExists(getAsymKeyPairAlias())) { + SecureKeyGenerator keyGenerator = SecureKeyGenerator.getInstance(mSecureConfig); + keyGenerator.generateAsymmetricKeyPair(getAsymKeyPairAlias()); + } + SecureKeyGenerator secureKeyGenerator = SecureKeyGenerator.getInstance(mSecureConfig); + EphemeralSecretKey secretKey = secureKeyGenerator.generateEphemeralDataKey(); + if(mSecureConfig.isDebugLoggingEnabled()) { + Log.i("FileCipher", "Calling: " + SecureRandom.class.getSimpleName() + + " EphemeralSecretKey: Base64:\n" + + Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT)); + } + SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig); + Pair encryptedData = secureCipher.encryptEphemeralData(secretKey, b, + keyPairAlias); + secureCipher.encryptSensitiveDataAsymmetric( + getAsymKeyPairAlias(), + secretKey.getEncoded(), + (byte[] encryptedEphemeralKey) -> { + byte[] encodedData = secureCipher.encodeEphemeralData( + getAsymKeyPairAlias().getBytes(), + encryptedEphemeralKey, + encryptedData.first, + encryptedData.second); + secretKey.destroy(); + try { + fileOutputStream.write(encodedData); + } catch (IOException e) { + Log.e(TAG, "Failed to write secure file."); + e.printStackTrace(); + } + }); + } + + @Override + public void write(int b) throws IOException { + throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED); + } + + @Override + public void write(@NonNull byte[] b, int off, int len) throws IOException { + throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED); + } + + @Override + public void close() throws IOException { + fileOutputStream.close(); + } + + @NonNull + @Override + public FileChannel getChannel() { + throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED); + } + + @Override + protected void finalize() throws IOException { + super.finalize(); + } + + @Override + public void flush() throws IOException { + fileOutputStream.flush(); + } + } + + /** + * Internal class adding encryption to reads + */ + class EncryptedFileInputStream extends FileInputStream { + private static final String READ_NOT_SUPPORTED = "For encrypted files, you must read all " + + "data simultaneously. Call #read(byte[])."; + + // Was 25 characters, truncating to fix compile error + private static final String TAG = "EncryptedFIS"; + + private FileInputStream fileInputStream; + private byte[] decryptedData; + private int readStatus = 0; + + EncryptedFileInputStream(FileInputStream fileInputStream) { + super(new FileDescriptor()); + this.fileInputStream = fileInputStream; + } + + @Override + public int read() throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + void decrypt(SecureContextCompat.EncryptedFileInputStreamListener listener) + throws IOException { + if (this.decryptedData == null) { + try { + byte[] encodedData = new byte[fileInputStream.available()]; + readStatus = fileInputStream.read(encodedData); + SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig); + secureCipher.decryptEncodedData(encodedData, decryptedData -> { + this.decryptedData = decryptedData; + //Binder.clearCallingIdentity(); + listener.onEncryptedFileInput(this); + }); + } catch (IOException ex) { + throw ex; + } + } + } + + private void destroyCache() { + if (decryptedData != null) { + Arrays.fill(decryptedData, (byte) 0); + decryptedData = null; + } + } + + @Override + public int read(@NonNull byte[] b) { + System.arraycopy(decryptedData, 0, b, 0, decryptedData.length); + return readStatus; + } + + // TODO, implement this + @Override + public int read(@NonNull byte[] b, int off, int len) throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + // TODO, implement this + @Override + public long skip(long n) throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + public int available() { + return decryptedData.length; + } + + @Override + public void close() throws IOException { + destroyCache(); + fileInputStream.close(); + } + + @Override + public FileChannel getChannel() { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + protected void finalize() throws IOException { + destroyCache(); + super.finalize(); + } + + @Override + public synchronized void mark(int readlimit) { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + public synchronized void reset() throws IOException { + throw new UnsupportedOperationException(READ_NOT_SUPPORTED); + } + + @Override + public boolean markSupported() { + return false; + } + + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureCipher.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureCipher.java new file mode 100644 index 0000000..a2a20b8 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureCipher.java @@ -0,0 +1,760 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.crypto; + + +import android.os.Build; +import android.security.keystore.KeyProperties; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.NonNull; + +import com.android.certifications.niap.niapsec.SecureConfig; +import com.android.certifications.niap.niapsec.biometric.BiometricSupport; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.MGF1ParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; + +/** + * Wraps {@link Cipher} to provide NIAP Sensitive data protection. + *

+ * Adds encryption options to encrypt data using both a data and key encryption key. + */ +public class SecureCipher { + + private static final String TAG = "SecureCipher"; + + private SecureConfig secureConfig; + + public static int MODE_ENCRYPT = 1; + public static int MODE_DECRYPT = 2; + + /** + * Generic type used to handle callbacks from the SecureCipher + */ + public interface SecureCallback { + } + + /** + * Callback interface for specifying authentication + */ + public interface SecureAuthCallback extends SecureCallback { + void authComplete(BiometricSupport.BiometricStatus status); + } + + /** + * Callback interface for specifying asynchronous and authenticated symmetric encryption + */ + public interface SecureSymmetricEncryptionCallback extends SecureCallback { + void encryptionComplete(byte[] cipherText, byte[] iv); + } + + /** + * Callback interface for specifying asynchronous and authenticated asymmetric encryption + */ + public interface SecureAsymmetricEncryptionCallback extends SecureCallback { + void encryptionComplete(byte[] cipherText); + } + + /** + * Callback interface for specifying asynchronous and authenticated decryption + */ + public interface SecureDecryptionCallback extends SecureCallback { + void decryptionComplete(byte[] clearText); + } + + /** + * Create and return a SecureCipher with NIAP recommended settings that requires biometric + * authentication for key use + * + * @param secureConfig The config to use + * @return A secure cipher with NIAP recommended settings + */ + public static SecureCipher getDefault(SecureConfig secureConfig) { + return new SecureCipher(secureConfig); + } + + /** + * Instantiates a SecureConfig with the provided config + * + * @param secureConfig The settings to build the cipher with + */ + private SecureCipher(SecureConfig secureConfig) { + this.secureConfig = secureConfig; + } + + /** + * Encoding types used internally + */ + enum SecureFileEncodingType { + SYMMETRIC(0), + ASYMMETRIC(1), + EPHEMERAL(2), + NOT_ENCRYPTED(1000); + + private final int type; + + SecureFileEncodingType(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public static SecureFileEncodingType fromId(int id) { + switch (id) { + case 0: + return SYMMETRIC; + case 1: + return ASYMMETRIC; + case 2: + return EPHEMERAL; + } + return NOT_ENCRYPTED; + } + + } + + /** + * Encrypts data with an existing key alias from the AndroidKeyStore. + * + * @param keyAlias The name of the existing SecretKey to retrieve from the AndroidKeyStore. + * @param clearData The unencrypted data to encrypt + * @return A Pair of byte[]'s, first is the encrypted data, second is the IV + * (initialization vector) used to encrypt which is required for decryption + */ + public void encryptSensitiveData(String keyAlias, + byte[] clearData, + SecureSymmetricEncryptionCallback callback) { + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(keyAlias, null); + Cipher cipher = Cipher.getInstance(secureConfig.getSymmetricCipherTransformation()); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] iv = cipher.getIV(); + if (secureConfig.isSymmetricRequireUserAuthEnabled()) { + secureConfig.getBiometricSupport().authenticate( + cipher, + (BiometricSupport.BiometricStatus status) -> { + switch (status) { + case SUCCESS: + try { + callback.encryptionComplete( + cipher.doFinal(clearData), + cipher.getIV()); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } + break; + default: + Log.i(TAG, "Failure"); + callback.encryptionComplete(null, null); + } + }); + } else { + callback.encryptionComplete(cipher.doFinal(clearData), cipher.getIV()); + } + } catch (GeneralSecurityException | IOException ex) { + Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } + } + + /** + * Encrypts data with a public key from the cert in the AndroidKeyStore. + * + * @param keyAlias The name of the existing KeyPair to retrieve the PublicKey from the + * AndroidKeyStore. + * @param clearData The unencrypted data to encrypt + * @return A Pair of byte[]'s, first is the encrypted data, second is the IV + * (initialization vector) used to encrypt which is required for decryption + */ + public void encryptSensitiveDataAsymmetric(String keyAlias, + byte[] clearData, + SecureAsymmetricEncryptionCallback callback) { + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey(); + Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation()); + if (secureConfig.getAsymmetricPaddings().equals( + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) { + cipher.init(Cipher.ENCRYPT_MODE, + publicKey, + new OAEPParameterSpec("SHA-256", + "MGF1", + new MGF1ParameterSpec("SHA-1"), + PSource.PSpecified.DEFAULT)); + } else { + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + } + byte[] clearText = cipher.doFinal(clearData); + callback.encryptionComplete(clearText); + } catch (GeneralSecurityException | IOException ex) { + Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } + } + + /** + * Encrypts data with a public key from the cert in the AndroidKeyStore. + * + * @param keyAlias The name of the existing KeyPair to retrieve the PublicKey from the + * AndroidKeyStore. + * @param clearData The unencrypted data to encrypt + * @return A Pair of byte[]'s, first is the encrypted data, second is the IV + * (initialization vector) used to encrypt which is required for decryption + */ + public byte[] encryptSensitiveDataAsymmetric(String keyAlias, + byte[] clearData) { + byte[] cipherText = null; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey(); + Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation()); + if (secureConfig.getAsymmetricPaddings().equals( + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) { + cipher.init(Cipher.ENCRYPT_MODE, + publicKey, + new OAEPParameterSpec("SHA-256", + "MGF1", + new MGF1ParameterSpec("SHA-1"), + PSource.PSpecified.DEFAULT)); + } else { + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + } + cipherText = cipher.doFinal(clearData); + } catch (GeneralSecurityException | IOException ex) { + Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } + return cipherText; + } + + /** + * Encrypts data using an Ephemeral key, destroying any trace of the key from the Cipher used. + * + * @param ephemeralSecretKey The generated Ephemeral key + * @param clearData The unencrypted data to encrypt + * @return A Pair of byte[]'s, first is the encrypted data, second is the IV + * (initialization vector) + * used to encrypt which is required for decryption + */ + public Pair encryptEphemeralData(EphemeralSecretKey ephemeralSecretKey, + byte[] clearData, String keyPairAlias) { + try { + SecureRandom secureRandom = null; + secureRandom = SecureRandom.getInstanceStrong(); + /* + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + } else { + secureRandom = new SecureRandom(); + } + */ + byte[] iv = new byte[SecureConfig.AES_IV_SIZE_BYTES]; + secureRandom.nextBytes(iv); + GCMParameterSpec parameterSpec = + new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(), iv); + final Cipher cipher = + Cipher.getInstance(secureConfig.getSymmetricCipherTransformation()); + cipher.init(Cipher.ENCRYPT_MODE, ephemeralSecretKey, parameterSpec); + byte[] encryptedData = cipher.doFinal(clearData); + ephemeralSecretKey.destroyCipherKey(cipher, Cipher.ENCRYPT_MODE, keyPairAlias); + return new Pair<>(encryptedData, iv); + } catch (GeneralSecurityException ex) { + Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } + } + + /** + * Decrypts a previously encrypted byte[] + *

+ * Destroys all traces of the key data in the Cipher. + * + * @param ephemeralSecretKey The generated Ephemeral key + * @param encryptedData The byte[] of encrypted data + * @param initializationVector The IV of which the encrypted data was encrypted with + * @return The byte[] of data that has been decrypted + */ + public byte[] decryptEphemeralData(EphemeralSecretKey ephemeralSecretKey, + byte[] encryptedData, byte[] initializationVector, + String keyPairAlias) { + try { + final Cipher cipher = + Cipher.getInstance(secureConfig.getSymmetricCipherTransformation()); + cipher.init(Cipher.DECRYPT_MODE, + ephemeralSecretKey, + new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(), + initializationVector)); + byte[] decryptedData = cipher.doFinal(encryptedData); + ephemeralSecretKey.destroyCipherKey(cipher, Cipher.DECRYPT_MODE, keyPairAlias); + return decryptedData; + } catch (GeneralSecurityException ex) { + Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } + } + + /** + * Decrypts a previously encrypted byte[] + * + * @param keyAlias The name of the existing SecretKey to retrieve from the + * AndroidKeyStore. + * @param encryptedData The byte[] of encrypted data + * @param initializationVector The IV of which the encrypted data was encrypted with + * @return The byte[] of data that has been decrypted + */ + public byte[] decryptSensitiveData(String keyAlias, byte[] encryptedData, + byte[] initializationVector) { + byte[] decryptedData = new byte[0]; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + Key key = keyStore.getKey(keyAlias, null); + Cipher cipher = Cipher.getInstance(secureConfig.getSymmetricCipherTransformation()); + GCMParameterSpec spec = new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(), + initializationVector); + cipher.init(Cipher.DECRYPT_MODE, key, spec); + decryptedData = cipher.doFinal(encryptedData); + } catch (GeneralSecurityException | IOException ex) { + Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } + return decryptedData; + } + + /** + * Decrypts a previously encrypted byte[] + * + * @param keyAlias The name of the existing SecretKey to retrieve from the + * AndroidKeyStore. + * @param encryptedData The byte[] of encrypted data + * @param initializationVector The IV of which the encrypted data was encrypted with + * @return The byte[] of data that has been decrypted + */ + public void decryptSensitiveData(String keyAlias, byte[] encryptedData, + byte[] initializationVector, + SecureDecryptionCallback callback) { + byte[] decryptedData = new byte[0]; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + Key key = keyStore.getKey(keyAlias, null); + Cipher cipher = Cipher.getInstance(secureConfig.getSymmetricCipherTransformation()); + GCMParameterSpec spec = new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(), + initializationVector); + cipher.init(Cipher.DECRYPT_MODE, key, spec); + if (secureConfig.isSymmetricRequireUserAuthEnabled()) { + secureConfig.getBiometricSupport().authenticate( + cipher, + (BiometricSupport.BiometricStatus status) -> { + switch (status) { + case SUCCESS: + try { + callback.decryptionComplete(cipher.doFinal(encryptedData)); + } catch (GeneralSecurityException e) { + Log.e(TAG, "Failure to decrypt data: " + + e.getLocalizedMessage()); + e.printStackTrace(); + } + break; + default: + Log.e(TAG, "Failure to decrypt data: " + + "Biometric authentication failed"); + callback.decryptionComplete(null); + } + }); + } else { + callback.decryptionComplete(cipher.doFinal(encryptedData)); + } + } catch (GeneralSecurityException ex) { + Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } catch (IOException ex) { + Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage()); + throw new SecurityException(ex); + } + } + + /** + * Decrypts a previously encrypted byte[] with the PrivateKey + * + * @param keyAlias The name of the existing KeyPair to retrieve from the AndroidKeyStore. + * @param encryptedData The byte[] of encrypted data + * @return The byte[] of data that has been decrypted + */ + public byte[] decryptSensitiveDataAsymmetric(String keyAlias, + byte[] encryptedData) { + byte[] clearData = new byte[0]; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, null); + Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation()); + if (secureConfig.getAsymmetricPaddings().equals( + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) { + cipher.init(Cipher.DECRYPT_MODE, + key, + new OAEPParameterSpec( + "SHA-256", + "MGF1", + new MGF1ParameterSpec("SHA-1"), + PSource.PSpecified.DEFAULT)); + } else { + cipher.init(Cipher.DECRYPT_MODE, key); + } + clearData = cipher.doFinal(encryptedData); + } catch (GeneralSecurityException | IOException ex) { + Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage()); + ex.printStackTrace(); + } + return clearData; + } + + /** + * Decrypts a previously encrypted byte[] with the PrivateKey + * + * @param keyAlias The name of the existing KeyPair to retrieve from the AndroidKeyStore. + * @param encryptedData The byte[] of encrypted data + * @return The byte[] of data that has been decrypted + */ + public void decryptSensitiveDataAsymmetric(String keyAlias, + byte[] encryptedData, + SecureDecryptionCallback callback) { + byte[] decryptedData = new byte[0]; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, null); + Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation()); + // If the key was created using the time-bound method, we must delay creation of the + // Cipher object + if (secureConfig.getAsymmetricRequireUserValiditySeconds() <= 0) { + if (secureConfig.getAsymmetricPaddings().equals( + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) { + cipher.init(Cipher.DECRYPT_MODE, + key, + new OAEPParameterSpec( + "SHA-256", + "MGF1", + new MGF1ParameterSpec("SHA-1"), + PSource.PSpecified.DEFAULT)); + } else { + cipher.init(Cipher.DECRYPT_MODE, key); + } + } + if (secureConfig.isAsymmetricRequireUserAuthEnabled()) { + if (secureConfig.getAsymmetricRequireUserValiditySeconds() <= 0) { + secureConfig.getBiometricSupport().authenticate( + cipher, + (BiometricSupport.BiometricStatus status) -> { + switch (status) { + case SUCCESS: + try { + byte[] clearData = cipher.doFinal(encryptedData); + callback.decryptionComplete(clearData); + + } catch (Exception e) { + Log.e(TAG, "Failure to decrypt data: " + + e.getLocalizedMessage()); + e.printStackTrace(); + } + break; + default: + Log.e(TAG, "Failure to decrypt data: " + + "Biometric authentication failed"); + callback.decryptionComplete(null); + throw new SecurityException("Failure to decrypt data: " + + "Biometric authentication failed"); + } + }); + } else { + secureConfig.getBiometricSupport().authenticate( + cipher, + (BiometricSupport.BiometricStatus status) -> { + switch (status) { + case SUCCESS: + try { + if (secureConfig.getAsymmetricPaddings().equals( + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) { + cipher.init(Cipher.DECRYPT_MODE, + key, + new OAEPParameterSpec( + "SHA-256", + "MGF1", + new MGF1ParameterSpec("SHA-1"), + PSource.PSpecified.DEFAULT)); + } else { + cipher.init(Cipher.DECRYPT_MODE, key); + } + byte[] clearData = cipher.doFinal(encryptedData); + callback.decryptionComplete(clearData); + + } catch (Exception e) { + Log.e(TAG, "Failure to decrypt data: " + + e.getLocalizedMessage()); + e.printStackTrace(); + } + break; + default: + Log.e(TAG, "Failure to decrypt data: " + + "Biometric authentication failed"); + callback.decryptionComplete(null); + throw new SecurityException("Failure to decrypt data: " + + "Biometric authentication failed"); + } + }); + + } + + + } else { + decryptedData = cipher.doFinal(encryptedData); + callback.decryptionComplete(decryptedData); + } + } catch (GeneralSecurityException | IOException ex) { + Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage()); + ex.printStackTrace(); + } + } + + /** + * Encode ephemeral data + * + * @param keyPairAlias The key pair alias of the RSA AndroidKeyStore used in encryption + * @param encryptedKey The encrypted key + * @param cipherText The encrypted data + * @param iv The IV created when the data was encrypted + * @return The encoded bytes + */ + public byte[] encodeEphemeralData(byte[] keyPairAlias, byte[] encryptedKey, + byte[] cipherText, byte[] iv) { + ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 4) + iv.length + + keyPairAlias.length + encryptedKey.length + cipherText.length); + byteBuffer.putInt(SecureFileEncodingType.EPHEMERAL.getType()); + byteBuffer.putInt(encryptedKey.length); + byteBuffer.put(encryptedKey); + byteBuffer.putInt(iv.length); + byteBuffer.put(iv); + byteBuffer.putInt(keyPairAlias.length); + byteBuffer.put(keyPairAlias); + byteBuffer.put(cipherText); + return byteBuffer.array(); + } + + /** + * Encodes encrypted data + * + * @param keyAlias The key pair alias of the RSA AndroidKeyStore key that the + * data encryption key was encrypted with + * @param cipherText The encrypted data + * @param iv The IV created during the initialal encrypt operation + * @return The encoded data + */ + public byte[] encodeSymmetricData(byte[] keyAlias, byte[] cipherText, byte[] iv) { + ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 3) + iv.length + + keyAlias.length + cipherText.length); + byteBuffer.putInt(SecureFileEncodingType.SYMMETRIC.getType()); + byteBuffer.putInt(iv.length); + byteBuffer.put(iv); + byteBuffer.putInt(keyAlias.length); + byteBuffer.put(keyAlias); + byteBuffer.put(cipherText); + return byteBuffer.array(); + } + + /** + * Encodes asymmetric data + * + * @param keyPairAlias The key pair alias of the RSA AndroidKeyStore key + * @param cipherText The cipher text that has been encrypted + * @return The encoded bytes + */ + public byte[] encodeAsymmetricData(byte[] keyPairAlias, byte[] cipherText) { + ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 2) + + keyPairAlias.length + cipherText.length); + byteBuffer.putInt(SecureFileEncodingType.ASYMMETRIC.getType()); + byteBuffer.putInt(keyPairAlias.length); + byteBuffer.put(keyPairAlias); + byteBuffer.put(cipherText); + return byteBuffer.array(); + } + + /** + * Decrypts previous encrypted/encoded {@link SecureCipher} + * + * @param encodedCipherText The encoded cipher text + * @return decrypted data + */ + public byte[] decryptEncodedData(byte[] encodedCipherText) { + byte[] clearText = new byte[0]; + ByteBuffer byteBuffer = ByteBuffer.wrap(encodedCipherText); + int encodingTypeVal = byteBuffer.getInt(); + SecureFileEncodingType encodingType = SecureFileEncodingType.fromId(encodingTypeVal); + byte[] encodedEphKey = null; + byte[] iv = null; + String keyAlias = null; + byte[] cipherText = null; + + switch (encodingType) { + case EPHEMERAL: + int encodedEphKeyLength = byteBuffer.getInt(); + encodedEphKey = new byte[encodedEphKeyLength]; + byteBuffer.get(encodedEphKey); + case SYMMETRIC: + int ivLength = byteBuffer.getInt(); + iv = new byte[ivLength]; + byteBuffer.get(iv); + case ASYMMETRIC: + int keyAliasLength = byteBuffer.getInt(); + byte[] keyAliasBytes = new byte[keyAliasLength]; + byteBuffer.get(keyAliasBytes); + keyAlias = new String(keyAliasBytes); + cipherText = new byte[byteBuffer.remaining()]; + byteBuffer.get(cipherText); + break; + case NOT_ENCRYPTED: + throw new SecurityException("Cannot determine file type."); + } + switch (encodingType) { + case EPHEMERAL: + final byte[] ephemeralCipherText = cipherText; + final byte[] ephemeralIv = iv; + String finalKeyAlias = keyAlias; + byte[] decryptedEphKey = decryptSensitiveDataAsymmetric(keyAlias, + encodedEphKey); + if (decryptedEphKey != null) { + EphemeralSecretKey ephemeralSecretKey = + new EphemeralSecretKey(decryptedEphKey, secureConfig); + clearText = decryptEphemeralData( + ephemeralSecretKey, + ephemeralCipherText, ephemeralIv, finalKeyAlias); + ephemeralSecretKey.destroy(); + } else { + Log.i(TAG, "Key was null, this usually means there was an auth " + + "failure."); + } + break; + case SYMMETRIC: + clearText = decryptSensitiveData( + keyAlias, + cipherText, iv); + break; + case ASYMMETRIC: + clearText = decryptSensitiveDataAsymmetric( + keyAlias, + cipherText); + break; + case NOT_ENCRYPTED: + throw new SecurityException("File not encrypted."); + } + return clearText; + } + + /** + * Decrypts previous encrypted/encoded {@link SecureCipher} + * + * @param encodedCipherText The encoded cipher text + * @param callback The callback, when the data is complete. + */ + public void decryptEncodedData(byte[] encodedCipherText, SecureDecryptionCallback callback) { + ByteBuffer byteBuffer = ByteBuffer.wrap(encodedCipherText); + int encodingTypeVal = byteBuffer.getInt(); + SecureFileEncodingType encodingType = SecureFileEncodingType.fromId(encodingTypeVal); + byte[] encodedEphKey = null; + byte[] iv = null; + String keyAlias = null; + byte[] cipherText = null; + + switch (encodingType) { + case EPHEMERAL: + int encodedEphKeyLength = byteBuffer.getInt(); + encodedEphKey = new byte[encodedEphKeyLength]; + byteBuffer.get(encodedEphKey); + case SYMMETRIC: + int ivLength = byteBuffer.getInt(); + iv = new byte[ivLength]; + byteBuffer.get(iv); + case ASYMMETRIC: + int keyAliasLength = byteBuffer.getInt(); + byte[] keyAliasBytes = new byte[keyAliasLength]; + byteBuffer.get(keyAliasBytes); + keyAlias = new String(keyAliasBytes); + cipherText = new byte[byteBuffer.remaining()]; + byteBuffer.get(cipherText); + break; + case NOT_ENCRYPTED: + throw new SecurityException("Cannot determine file type."); + } + switch (encodingType) { + case EPHEMERAL: + final byte[] ephemeralCipherText = cipherText; + final byte[] ephemeralIv = iv; + String finalKeyAlias = keyAlias; + decryptSensitiveDataAsymmetric(keyAlias, + encodedEphKey, + (byte[] decryptedEphKey) -> { + if (decryptedEphKey != null) { + EphemeralSecretKey ephemeralSecretKey = + new EphemeralSecretKey(decryptedEphKey, secureConfig); + byte[] decrypted = decryptEphemeralData( + ephemeralSecretKey, + ephemeralCipherText, ephemeralIv, finalKeyAlias); + callback.decryptionComplete(decrypted); + ephemeralSecretKey.destroy(); + } else { + Log.i(TAG, "Key was null, this usually means there was an auth " + + "failure."); + } + }); + + break; + case SYMMETRIC: + decryptSensitiveData( + keyAlias, + cipherText, iv, callback); + break; + case ASYMMETRIC: + decryptSensitiveDataAsymmetric( + keyAlias, + cipherText, callback); + break; + case NOT_ENCRYPTED: + throw new SecurityException("File not encrypted."); + } + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyGenerator.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyGenerator.java new file mode 100644 index 0000000..09aa5cf --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyGenerator.java @@ -0,0 +1,160 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.crypto; + +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Log; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; + +import javax.crypto.KeyGenerator; + +/** + * Class used for generating ephemeral keys + */ +public class SecureKeyGenerator { + + private static final String TAG = "SecureKeyGenerator"; + + private SecureConfig secureConfig; + + /** + * Create an instance of the key generator with custom settings + * + * @param secureConfig The config to use when building the key generator + * @return The key generator + */ + public static SecureKeyGenerator getInstance(SecureConfig secureConfig) { + return new SecureKeyGenerator(secureConfig); + } + + private SecureKeyGenerator(SecureConfig secureConfig) { + this.secureConfig = secureConfig; + } + + /** + *

+ * Generates a sensitive data key and adds the SecretKey to the AndroidKeyStore. + * Utilizes UnlockedDeviceProtection to ensure that the device must be unlocked in order to + * use the generated key. + *

+ * + * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore. + * @return true if the key was generated, false otherwise + */ + public boolean generateKey(String keyAlias) { + boolean created = false; + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance( + secureConfig.getSymmetricKeyAlgorithm(), + secureConfig.getAndroidKeyStore()); + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( + keyAlias, secureConfig.getSymmetricKeyPurposes()). + setBlockModes(secureConfig.getSymmetricBlockModes()). + setEncryptionPaddings(secureConfig.getSymmetricPaddings()). + setKeySize(secureConfig.getSymmetricKeySize()); + builder = builder.setUserAuthenticationRequired( + secureConfig.isSymmetricRequireUserAuthEnabled()); + builder = builder.setUserAuthenticationValidityDurationSeconds( + secureConfig.getSymmetricRequireUserValiditySeconds()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder = builder.setUnlockedDeviceRequired( + secureConfig.isSymmetricSensitiveDataProtectionEnabled()); + } + keyGenerator.init(builder.build()); + keyGenerator.generateKey(); + created = true; + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | + NoSuchProviderException ex) { + throw new SecurityException(ex); + } + return created; + } + + /** + *

+ * Generates a sensitive data public/private key pair and adds the KeyPair to the AndroidKeyStore. + * Utilizes UnlockedDeviceProtection to ensure that the device must be unlocked in order to + * use the generated key. + *

+ *

+ * ANDROID P ONLY (API LEVEL 28>) + *

+ * + * @param keyPairAlias The name of the generated SecretKey to save into the AndroidKeyStore. + * @return true if the key was generated, false otherwise + */ + public boolean generateAsymmetricKeyPair(String keyPairAlias) { + boolean created = false; + try { + KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance( + secureConfig.getAsymmetricKeyPairAlgorithm(), + secureConfig.getAndroidKeyStore()); + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( + keyPairAlias, secureConfig.getAsymmetricKeyPurposes()) + .setEncryptionPaddings(secureConfig.getAsymmetricPaddings()) + .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) + .setBlockModes(secureConfig.getAsymmetricBlockModes()) + .setKeySize(secureConfig.getAsymmetricKeySize()); + builder = builder.setUserAuthenticationRequired( + secureConfig.isAsymmetricRequireUserAuthEnabled()); + builder = builder.setUserAuthenticationValidityDurationSeconds( + secureConfig.getAsymmetricRequireUserValiditySeconds()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder = builder.setUnlockedDeviceRequired( + secureConfig.isAsymmetricSensitiveDataProtectionEnabled()); + } + keyGenerator.initialize(builder.build()); + keyGenerator.generateKeyPair(); + created = true; + } catch (NoSuchProviderException | + InvalidAlgorithmParameterException | + NoSuchAlgorithmException ex) { + throw new SecurityException(ex); + } + return created; + } + + /** + *

+ * Generates an Ephemeral symmetric key that can be fully destroyed and removed from memory. + *

+ * + * @return The EphemeralSecretKey generated + */ + public EphemeralSecretKey generateEphemeralDataKey() { + try { + SecureRandom secureRandom = SecureRandom.getInstanceStrong(); + byte[] key = new byte[secureConfig.getSymmetricKeySize() / 8]; + secureRandom.nextBytes(key); + return new EphemeralSecretKey(key, + secureConfig.getSymmetricKeyAlgorithm(), + secureConfig); + } catch (GeneralSecurityException ex) { + throw new SecurityException(ex); + } + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyStore.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyStore.java new file mode 100644 index 0000000..103923e --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyStore.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.crypto; + +import android.security.keystore.KeyInfo; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.PrivateKey; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; + +/** + * Wrapper for the AndroidKeyStore to easily generate keys + */ +public class SecureKeyStore { + + private static final String TAG = "SecureKeyStore"; + + private SecureConfig secureConfig; + + /** + * Gets the key store wrapper with recommended NIAP settings + * + * @return The key store wrapper + */ + public static SecureKeyStore getDefault(SecureConfig secureConfig) { + return new SecureKeyStore(secureConfig); + } + + private SecureKeyStore(SecureConfig secureConfig) { + this.secureConfig = secureConfig; + } + + public boolean keyExists(String keyAlias) { + boolean exists = false; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + exists = keyStore.getCertificate(keyAlias).getPublicKey() != null; + } catch (GeneralSecurityException | IOException ex) { + throw new SecurityException(ex); + } + return exists; + } + + /** + * Checks to see if the specified key is stored in secure hardware + * + * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore. + * @return true if the key is stored in secure hardware + */ + public boolean checkKeyInsideSecureHardware(String keyAlias) { + boolean inHardware = false; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(keyAlias, null); + SecretKeyFactory factory = SecretKeyFactory.getInstance(key.getAlgorithm(), + secureConfig.getAndroidKeyStore()); + KeyInfo keyInfo; + keyInfo = (KeyInfo) factory.getKeySpec(key, KeyInfo.class); + inHardware = keyInfo.isInsideSecureHardware(); + return inHardware; + } catch (GeneralSecurityException | IOException e) { + return inHardware; + } + } + + /** + * Checks to see if the specified private key is stored in secure hardware + * + * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore. + * @return true if the key is stored in secure hardware + */ + public boolean checkKeyInsideSecureHardwareAsymmetric(String keyAlias) { + boolean inHardware = false; + try { + KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore()); + keyStore.load(null); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, null); + + KeyFactory factory = KeyFactory.getInstance(privateKey.getAlgorithm(), + secureConfig.getAndroidKeyStore()); + KeyInfo keyInfo; + + keyInfo = factory.getKeySpec(privateKey, KeyInfo.class); + inHardware = keyInfo.isInsideSecureHardware(); + return inHardware; + + } catch (GeneralSecurityException | IOException e) { + return inHardware; + } + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/CertificateValidation.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/CertificateValidation.java new file mode 100644 index 0000000..d808656 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/CertificateValidation.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 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.certifications.niap.niapsec.net; + +/** + * A Configuration Class for the NIAPSEC network library + */ +public class CertificateValidation { + static boolean enableOCSPStaplingCheck = true; + static boolean enableCpvCheck = false; + static boolean cpvCheckDoNotFallbackToCrl = false; + static boolean enforceTlsV1_2 = true; +} \ No newline at end of file diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureKeyManager.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureKeyManager.java new file mode 100644 index 0000000..7abf24e --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureKeyManager.java @@ -0,0 +1,180 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.net; + + +import android.app.Activity; +import android.content.Intent; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.security.KeyChainException; +import android.util.Log; + +import androidx.annotation.Nullable; +import com.android.certifications.niap.niapsec.SecureConfig; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; + +import javax.net.ssl.X509KeyManager; + +import java.security.cert.X509Certificate; + +/** + * Used internally by ValidatableSSLSocketFactory to handle cert chains and keys + */ +class SecureKeyManager implements X509KeyManager, KeyChainAliasCallback { + private static final String TAG = "SecureKeyManager"; + + private final String alias; + private final SecureConfig secureConfig; + + private X509Certificate[] certChain; + private PrivateKey privateKey; + private static Activity ACTIVITY; + + public static void setContext(Activity activity) { + ACTIVITY = activity; + } + + public enum CertType { + X509(0), + PKCS12(1), + NOT_SUPPORTED(1000); + + private final int type; + + CertType(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public static CertType fromId(int id) { + switch (id) { + case 0: + return X509; + case 1: + return PKCS12; + } + return NOT_SUPPORTED; + } + } + + + public static SecureKeyManager getDefault(String alias) { + return getDefault(alias, SecureConfig.getStrongConfig()); + } + + public static SecureKeyManager getDefault(String alias, SecureConfig secureConfig) { + SecureKeyManager keyManager = new SecureKeyManager(alias, secureConfig); + try { + KeyChain.choosePrivateKeyAlias(ACTIVITY, keyManager, + secureConfig.getClientCertAlgorithms(), null, null, -1, alias); + } catch (Exception ex) { + Log.i(TAG, "Failed to retrieve client cert: " + ex.getMessage()); + } + return keyManager; + } + + public static SecureKeyManager installCertManually(CertType certType, + byte[] certData, + String keyAlias, + SecureConfig secureConfig) { + SecureKeyManager keyManager = new SecureKeyManager(keyAlias, secureConfig); + Intent intent = KeyChain.createInstallIntent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + switch (certType) { + case X509: + intent.putExtra(KeyChain.EXTRA_CERTIFICATE, certData); + break; + case PKCS12: + intent.putExtra(KeyChain.EXTRA_PKCS12, certData); + break; + default: + throw new SecurityException("Cert type not supported."); + } + ACTIVITY.startActivity(intent); + return keyManager; + } + + public SecureKeyManager(String alias, SecureConfig secureConfig) { + this.alias = alias; + this.secureConfig = secureConfig; + } + + @Override + public String chooseClientAlias(String[] arg0, Principal[] arg1, Socket arg2) { + return alias; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + if (this.alias.equals(alias)) return certChain; + return null; + } + + public void setCertChain(X509Certificate[] certChain) { + this.certChain = certChain; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + if (this.alias.equals(alias)) return privateKey; + return null; + } + + public void setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + } + + @Override + public final String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + throw new UnsupportedOperationException(); + } + + @Override + public final String[] getClientAliases(String keyType, Principal[] issuers) { + throw new UnsupportedOperationException(); + } + + @Override + public final String[] getServerAliases(String keyType, Principal[] issuers) { + throw new UnsupportedOperationException(); + } + + @Override + public void alias(@Nullable String alias) { + try { + assert alias != null; + certChain = KeyChain.getCertificateChain(ACTIVITY.getApplicationContext(), alias); + privateKey = KeyChain.getPrivateKey(ACTIVITY.getApplicationContext(), alias); + if (certChain == null || privateKey == null) { + throw new SecurityException("Could not retrieve the cert chain and " + + "private key from client cert."); + } + this.setCertChain(certChain); + this.setPrivateKey(privateKey); + } catch (KeyChainException | InterruptedException ex) { + throw new SecurityException("Could not retrieve the cert chain and " + + "private key from client cert."); + } + } +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureURL.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureURL.java new file mode 100644 index 0000000..64c9c57 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureURL.java @@ -0,0 +1,315 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.net; + +import android.util.Log; + +import com.android.certifications.niap.niapsec.SecureConfig; +import com.android.certifications.niap.niapsec.config.TldConstants; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.PKIXParameters; +import java.security.cert.PKIXRevocationChecker; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocket; + +/** + * Wraps URL to provide automatic cert revocation checking through OCSP and enforces TLS use. + */ +public class SecureURL { + + private static final String TAG = "SecureURL"; + + private final URL url; + private final SecureConfig secureConfig; + private final String clientCertAlias; + + /** + * Creates a SecureURL + * + * @param spec The URL spec + * @param clientCertAlias The cert alias of the client cert to be used (The parameter for KeyChain.choosePrivateKeyAlias), accept null + * @throws MalformedURLException If the spec is malformed and not valid URL + */ + public SecureURL(String spec, String clientCertAlias) throws MalformedURLException { + this(spec, clientCertAlias, SecureConfig.getStrongConfig()); + } + + /** + * Creates a SecureURL + * + * @param spec The URL spec + * @param clientCertAlias The cert alias of the client cert to be used + * @param secureConfig The provided settings to configure TLS version + * @throws MalformedURLException If the spec is malformed and not valid URL + */ + public SecureURL(String spec, String clientCertAlias, SecureConfig secureConfig) + throws MalformedURLException { + this.url = new URL(addProtocol(spec)); + this.clientCertAlias = clientCertAlias; + this.secureConfig = secureConfig; + } + + /** + * Open the TLS connection to the host specified at construction + * + * @return The connection that has been established + * @throws IOException when there is a bad host name or TLS configuration + */ + public URLConnection openConnection() throws IOException { + Log.i(TAG, SecureConfig.PACKAGE_NAME + + " initiated a trusted channel to " + getHostname()); + HttpsURLConnection urlConnection = (HttpsURLConnection) this.url.openConnection(); + urlConnection.setSSLSocketFactory(new ValidatableSSLSocketFactory(this)); + Log.i(TAG, "TLS session established and validated to " + getHostInfo()); + return urlConnection; + } + + /** + * Checks the hostname against an open SSLSocket connect to the hostname for validity for certs + * and hostname validity. + * + * @param trustedCAs List of trustedCA files to use + * @return URLConnection if the SSLSocket has a valid cert and if the hostname is valid + */ + public URLConnection openUserTrustedCertConnection(Map trustedCAs) + throws IOException { + Log.i(TAG, SecureConfig.PACKAGE_NAME + " initiated a trusted channel to " + + getHostname()); + HttpsURLConnection urlConnection = (HttpsURLConnection) this.url.openConnection(); + urlConnection.setSSLSocketFactory( + new ValidatableSSLSocketFactory(this, trustedCAs, secureConfig)); + Log.i(TAG, "TLS session established and validated to " + getHostInfo()); + return urlConnection; + } + + /** + * Checks the hostname against an open SSLSocket connect to the hostname for validity for certs + * and hostname validity. + * + * @param hostname The host name to check + * @param socket The SSLSocket that is open to the URL of the host to check + * @return true if the SSLSocket has a valid cert and if the hostname is valid, false otherwise. + */ + public boolean isValid(String hostname, SSLSocket socket) { + try { + boolean valid = isValid(Arrays.asList(socket.getSession().getPeerCertificates())); + boolean hostnameValid = HttpsURLConnection.getDefaultHostnameVerifier() + .verify(hostname, socket.getSession()); + if(!valid) { + Log.i(TAG, "Failed to validate X509v3 certificates. "+ getHostInfo()); + } else if(!hostnameValid) { + Log.i(TAG, "Failed to validate presented identifier. "+getHostInfo()); + } + return hostnameValid + && valid + && validTldWildcards(Arrays.asList(socket.getSession().getPeerCertificates())); + } catch (SSLPeerUnverifiedException e) { + Log.i(TAG, "Validity Check failed: "+ getHostInfo() + " Ex:" + e.getMessage()); + //e.printStackTrace(); + return false; + } + } + + /** + * Checks the HttpsUrlConnection certificates for validity. + *

+ * Example Code: + * URL url = new URL("https://" + urlText); + * conn = (HttpsURLConnection) url.openConnection(); + * boolean valid = SecureURL.isValid(conn); + *

+ * + * @param conn The connection to check the certificates for + * @return true if the certificates for the HttpsUrlConnection are valid, false otherwise + */ + public boolean isValid(HttpsURLConnection conn) { + try { + return isValid(Arrays.asList(conn.getServerCertificates())) && + validTldWildcards(Arrays.asList(conn.getServerCertificates())); + } catch (SSLPeerUnverifiedException e) { + Log.i(TAG, "Valid Check failed: " + e.getMessage()); + e.fillInStackTrace(); + return false; + } + } + + /** + * Method to check a certificate for validity. + * + * @param cert cert to check + * @return true if the certs are valid, false otherwise + */ + public boolean isValid(Certificate cert) { + List certs = new ArrayList<>(); + certs.add(cert); + return isValid(certs); + } + + + + /** + * Method to check a list of certificates for validity. + * If certificate revocation check can only use OCSP verification, it works. + * Otherwise PKIXRevocationChecker raise errors. + * + * @param certs list of certs to check + * @return true if the certs are valid, false otherwise + */ + public boolean isValid(List certs) { + try { + List leafCerts = new ArrayList<>(); + for (Certificate cert : certs) { + if (!isRootCA(cert)) { + leafCerts.add(cert); + } + } + CertPath path = CertificateFactory.getInstance(secureConfig.getCertPath()) + .generateCertPath(leafCerts); + KeyStore ks = KeyStore.getInstance(secureConfig.getAndroidCAStore()); + try { + ks.load(null, null); + } catch (IOException e) { + e.fillInStackTrace(); + throw new AssertionError(e); + } + CertPathValidator cpv = CertPathValidator.getInstance( + secureConfig.getCertPathValidator());//=PKIX by default + PKIXParameters params = new PKIXParameters(ks); + params.setRevocationEnabled(true); + + PKIXRevocationChecker checker = (PKIXRevocationChecker) cpv.getRevocationChecker(); + if(CertificateValidation.cpvCheckDoNotFallbackToCrl) { + checker.setOptions(EnumSet.of + (PKIXRevocationChecker.Option.NO_FALLBACK, PKIXRevocationChecker.Option.SOFT_FAIL)); + } + params.addCertPathChecker(checker); + cpv.validate(path, params); + + return true; + } catch (CertPathValidatorException e) { + // If you see "Unable to determine revocation status due to network error" + // Make sure your network security config allows for clear text access of the relevant + // OCSP url. + e.fillInStackTrace(); + return false; + } catch (GeneralSecurityException e) { + e.fillInStackTrace(); + return false; + } + } + + /** + * Get the hostname of the URL specified at construction + * + * @return The hostname of the URL + */ + public String getHostname() { + return this.url.getHost(); + } + + /** + * Get the host from the URL, and the package name of the app for logging purposes + * + * @return The host name concatenated with the package name. + */ + public String getHostInfo() { + return "Host: "+ this.url.getHost() + " App: " + SecureConfig.PACKAGE_NAME; + } + + /** + * Gets the client cert alias + * + * @return The client cert alias given at construction + */ + public String getClientCertAlias() { + return this.clientCertAlias; + } + + private String addProtocol(String spec) { + if (!spec.toLowerCase().startsWith("http://") && + !spec.toLowerCase().startsWith("https://")) { + return "https://" + spec; + } + return spec; + } + + private boolean isRootCA(Certificate cert) { + boolean rootCA = false; + if (cert instanceof X509Certificate) { + X509Certificate x509Certificate = (X509Certificate) cert; + if (x509Certificate.getSubjectDN().getName() + .equals(x509Certificate.getIssuerDN().getName())) { + rootCA = true; + } + } + return rootCA; + } + + private boolean validTldWildcards(List certs) { + // For a more complete list https://publicsuffix.org/list/public_suffix_list.dat + for (Certificate cert : certs) { + if (cert instanceof X509Certificate) { + X509Certificate x509Cert = (X509Certificate) cert; + try { + Collection> subAltNames = x509Cert.getSubjectAlternativeNames(); + if (subAltNames != null) { + List dnsNames = new ArrayList<>(); + for (List tldList : subAltNames) { + if (tldList.size() >= 2) { + dnsNames.add(tldList.get(1).toString().toUpperCase()); + } + } + // Populate DNS NAMES, make sure they are lower case + for (String dnsName : dnsNames) { + if (TldConstants.VALID_TLDS.contains(dnsName)) { + Log.i(TAG, "FAILED WILDCARD TldConstants CHECK: " + dnsName); + return false; + } + } + } + } catch (CertificateParsingException ex) { + Log.i(TAG, "Cert Parsing Issue: " + ex.getMessage()); + return false; + } + } + } + return true; + } + +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocket.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocket.java new file mode 100644 index 0000000..e71493f --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocket.java @@ -0,0 +1,451 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.net; + + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.channels.SocketChannel; +import java.security.Security; +import java.util.List; + +import javax.net.ssl.HandshakeCompletedEvent; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + + +/** + * Custom SSLSocket that automatically handles OCSP validation. + *

+ * Internal Only + */ +class ValidatableSSLSocket extends SSLSocket { + + private static final String TAG = "ValidatableSSLSocket"; + + private final SSLSocket sslSocket; + private final String hostname; + private final SecureURL secureURL; + private boolean handshakeStarted = false; + private final SecureConfig secureConfig; + + public ValidatableSSLSocket(SecureURL secureURL, + Socket sslSocket, + SecureConfig secureConfig, + String[] supportedCipherSuites) throws IOException { + this.secureURL = secureURL; + this.hostname = secureURL.getHostname(); + this.sslSocket = (SSLSocket) sslSocket; + this.secureConfig = secureConfig; + setSecureCiphers(supportedCipherSuites); + try { + isValid(); + } catch (IOException ex) { + Log.i(TAG, "Failed to establish a TLS connection to " + + secureURL.getHostname() + " ... " + ex.getMessage()); + throw ex; + } + } + + private void setSecureCiphers(String[] supportedCipherSuites) { + if (secureConfig.isUseStrongSSLCiphersEnabled()) { + this.sslSocket.setEnabledCipherSuites(supportedCipherSuites); + } + } + + private void isValid() throws IOException { + + //Check OCSP stapling status from the handshake response. + sslSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() { + @Override + public void handshakeCompleted(HandshakeCompletedEvent event) { + if(!CertificateValidation.enableOCSPStaplingCheck) + return; + SSLSession session = event.getSession(); + try { + //Call hidden api. https://www.xda-developers.com/bypass-hidden-apis/ + //java.util.List com.android.org.conscrypt.Java7ExtendedSSLSession.getStatusResponses() + Method m = session.getClass().getMethod("getStatusResponses"); + m.setAccessible(true); + List resp = (List)m.invoke(session); + //if the response is empty the server doesn't support ocsp stapling=>fail + if(resp == null || resp.isEmpty()){ + throw new AssertionError("Assertion:Found no OCSP stapling response from server."); + } + //Returns APN.1 byte code, but it has already checked by network stack. + //So we don't need further check here + } catch (NoSuchMethodException | InvocationTargetException | + IllegalAccessException e) { + throw new RuntimeException("ConscryptSSLSession.getStatusResponses() is not supported.",e); + } + } + }); + Security.setProperty("ocsp.enable", "true"); + startHandshake(); + + ////////////////////////////////////////////////////////////////////// + // CPV(CertPathValidator) revocation check almost always fail with android today. + // + // 1: Java ssl socket doesn't allow OCSP validation + // if any intermediate cert doesn't support it. + // 2: CRL validation require plain text connection. we won't allow it. + // + // You may use the check in exclusive situation only when you know what it means. + // + if(CertificateValidation.enableCpvCheck) { + try { + if (!secureURL.isValid(this.hostname, this.sslSocket)) { + throw new IOException("Found invalid certificate"); + } + } catch (IOException ex) { + ex.fillInStackTrace(); + throw new IOException("Found invalid certificate"); + } + } + + } + + @Override + public void startHandshake() throws IOException { + if (!handshakeStarted) { + sslSocket.startHandshake(); + handshakeStarted = true; + } + } + + @Override + public String[] getSupportedCipherSuites() { + return sslSocket.getSupportedCipherSuites(); + } + + @Override + public String[] getEnabledCipherSuites() { + return sslSocket.getEnabledCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + sslSocket.setEnabledCipherSuites(suites); + } + + @Override + public String[] getSupportedProtocols() { + return sslSocket.getSupportedProtocols(); + } + + @Override + public String[] getEnabledProtocols() { + return sslSocket.getEnabledProtocols(); + } + + @Override + public void setEnabledProtocols(String[] protocols) { + sslSocket.setEnabledProtocols(protocols); + } + + @Override + public SSLSession getSession() { + return sslSocket.getSession(); + } + + @Override + public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { + sslSocket.addHandshakeCompletedListener(listener); + } + + @Override + public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { + sslSocket.removeHandshakeCompletedListener(listener); + } + + @Override + public void setUseClientMode(boolean mode) { + sslSocket.setUseClientMode(mode); + } + + @Override + public boolean getUseClientMode() { + return sslSocket.getUseClientMode(); + } + + @Override + public void setNeedClientAuth(boolean need) { + sslSocket.setNeedClientAuth(need); + } + + @Override + public boolean getNeedClientAuth() { + return sslSocket.getNeedClientAuth(); + } + + @Override + public void setWantClientAuth(boolean want) { + sslSocket.setWantClientAuth(want); + } + + @Override + public boolean getWantClientAuth() { + return sslSocket.getWantClientAuth(); + } + + @Override + public void setEnableSessionCreation(boolean flag) { + sslSocket.setEnableSessionCreation(flag); + } + + @Override + public boolean getEnableSessionCreation() { + return sslSocket.getEnableSessionCreation(); + } + + @Override + public SSLSession getHandshakeSession() { + return sslSocket.getHandshakeSession(); + } + + @Override + public SSLParameters getSSLParameters() { + return sslSocket.getSSLParameters(); + } + + @Override + public void setSSLParameters(SSLParameters params) { + sslSocket.setSSLParameters(params); + } + + @NonNull + @Override + public String toString() { + return sslSocket.toString(); + } + + @Override + public void connect(SocketAddress endpoint) throws IOException { + sslSocket.connect(endpoint); + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + sslSocket.connect(endpoint, timeout); + } + + @Override + public void bind(SocketAddress bindpoint) throws IOException { + sslSocket.bind(bindpoint); + } + + @Override + public InetAddress getInetAddress() { + return sslSocket.getInetAddress(); + } + + @Override + public InetAddress getLocalAddress() { + return sslSocket.getLocalAddress(); + } + + @Override + public int getPort() { + return sslSocket.getPort(); + } + + @Override + public int getLocalPort() { + return sslSocket.getLocalPort(); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return sslSocket.getRemoteSocketAddress(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return sslSocket.getLocalSocketAddress(); + } + + @Override + public SocketChannel getChannel() { + return sslSocket.getChannel(); + } + + @Override + public InputStream getInputStream() throws IOException { + return sslSocket.getInputStream(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return sslSocket.getOutputStream(); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + sslSocket.setTcpNoDelay(on); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return sslSocket.getTcpNoDelay(); + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + sslSocket.setSoLinger(on, linger); + } + + @Override + public int getSoLinger() throws SocketException { + return sslSocket.getSoLinger(); + } + + @Override + public void sendUrgentData(int data) throws IOException { + sslSocket.sendUrgentData(data); + } + + @Override + public void setOOBInline(boolean on) throws SocketException { + sslSocket.setOOBInline(on); + } + + @Override + public boolean getOOBInline() throws SocketException { + return sslSocket.getOOBInline(); + } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + sslSocket.setSoTimeout(timeout); + } + + @Override + public synchronized int getSoTimeout() throws SocketException { + return sslSocket.getSoTimeout(); + } + + @Override + public synchronized void setSendBufferSize(int size) throws SocketException { + sslSocket.setSendBufferSize(size); + } + + @Override + public synchronized int getSendBufferSize() throws SocketException { + return sslSocket.getSendBufferSize(); + } + + @Override + public synchronized void setReceiveBufferSize(int size) throws SocketException { + sslSocket.setReceiveBufferSize(size); + } + + @Override + public synchronized int getReceiveBufferSize() throws SocketException { + return sslSocket.getReceiveBufferSize(); + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + sslSocket.setKeepAlive(on); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return sslSocket.getKeepAlive(); + } + + @Override + public void setTrafficClass(int tc) throws SocketException { + sslSocket.setTrafficClass(tc); + } + + @Override + public int getTrafficClass() throws SocketException { + return sslSocket.getTrafficClass(); + } + + @Override + public void setReuseAddress(boolean on) throws SocketException { + sslSocket.setReuseAddress(on); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return sslSocket.getReuseAddress(); + } + + @Override + public synchronized void close() throws IOException { + sslSocket.close(); + Log.i(TAG, "TLS session terminated for " + secureURL.getHostInfo()); + } + + @Override + public void shutdownInput() throws IOException { + sslSocket.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + sslSocket.shutdownOutput(); + } + + @Override + public boolean isConnected() { + return sslSocket.isConnected(); + } + + @Override + public boolean isBound() { + return sslSocket.isBound(); + } + + @Override + public boolean isClosed() { + return sslSocket.isClosed(); + } + + @Override + public boolean isInputShutdown() { + return sslSocket.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return sslSocket.isOutputShutdown(); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + sslSocket.setPerformancePreferences(connectionTime, latency, bandwidth); + } +} diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactory.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactory.java new file mode 100644 index 0000000..420523e --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactory.java @@ -0,0 +1,246 @@ +/* + * Copyright 2020 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.certifications.niap.niapsec.net; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import static com.android.certifications.niap.niapsec.SecureConfig.SSL_TLS; + +import android.util.Log; + + +/** + * Custom SSLSocketFactory that uses ValidatableSSLSocket to handle automatic cert revocation + * checking and forces TLS use. + *

+ * Internal only + */ +class ValidatableSSLSocketFactory extends SSLSocketFactory { + + final private static String TAG = "ValidatableSSLSocketFactory"; + + private final SSLSocketFactory sslSocketFactory; + private final SecureURL secureURL; + private final SecureConfig secureConfig; + private final String[] supportedSecureCipherSuites; + + private Socket socket; + + public ValidatableSSLSocketFactory(SecureURL secureURL, + SSLSocketFactory sslSocketFactory, + SecureConfig secureConfig) { + SSLSocketFactory sslSocketFactory1; + this.secureURL = secureURL; + + if(CertificateValidation.enforceTlsV1_2){ + try { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null,null,null); + sslSocketFactory1 = sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException ex){ + Log.i(TAG,"Couldn't find TLSv1.2 algo."); + sslSocketFactory1 = sslSocketFactory; + } + } else { + sslSocketFactory1 = sslSocketFactory; + } + + this.sslSocketFactory = sslSocketFactory1; + this.secureConfig = secureConfig; + this.supportedSecureCipherSuites = getSecureSupportedCipherSuites(); + } + + public ValidatableSSLSocketFactory(SecureURL secureURL, SSLSocketFactory sslSocketFactory) { + this(secureURL, sslSocketFactory, SecureConfig.getStrongConfig()); + } + + public ValidatableSSLSocketFactory(SecureURL secureURL) { + this(secureURL, + (SSLSocketFactory) SSLSocketFactory.getDefault(), + SecureConfig.getStrongConfig()); + } + + public ValidatableSSLSocketFactory(SecureURL secureURL, + Map trustedCAs, + SecureConfig secureConfig) { + this(secureURL, + createUserTrustSSLSocketFactory(trustedCAs, secureConfig, secureURL), + secureConfig); + } + + private String[] getSecureSupportedCipherSuites() { + List supportedCipherSuites = Arrays.asList(getSupportedCipherSuites()); + List supportedSecureCipherSuites = new ArrayList<>(); + for(String cipherSuite : this.secureConfig.getStrongSSLCiphers()) { + if(supportedCipherSuites.contains(cipherSuite)) { + supportedSecureCipherSuites.add(cipherSuite); + } + } + return supportedSecureCipherSuites.toArray(new String[0]); + } + + @Override + public String[] getDefaultCipherSuites() { + return sslSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + if (socket == null) { + socket = new ValidatableSSLSocket(secureURL, + sslSocketFactory.createSocket(s, host, port, autoClose), + secureConfig, + supportedSecureCipherSuites); + } + return socket; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + if (socket == null) { + socket = new ValidatableSSLSocket(secureURL, + sslSocketFactory.createSocket(host, port), + secureConfig, + supportedSecureCipherSuites); + } + return socket; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException { + if (socket == null) { + socket = new ValidatableSSLSocket(secureURL, + sslSocketFactory.createSocket(host, port, localHost, localPort), + secureConfig, + supportedSecureCipherSuites); + } + return socket; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + if (socket == null) { + socket = new ValidatableSSLSocket(secureURL, + sslSocketFactory.createSocket(host, port), + secureConfig, + supportedSecureCipherSuites); + } + return socket; + } + + @Override + public Socket createSocket(InetAddress address, + int port, + InetAddress localAddress, + int localPort) throws IOException { + if (socket == null) { + socket = new ValidatableSSLSocket(secureURL, + sslSocketFactory.createSocket(address, port, localAddress, localPort), + secureConfig, + supportedSecureCipherSuites); + } + return socket; + } + + private static SSLSocketFactory createUserTrustSSLSocketFactory( + Map trustAnchors, + SecureConfig secureConfig, + SecureURL secureURL) { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + + KeyStore clientStore = KeyStore.getInstance(secureConfig.getKeystoreType()); + clientStore.load(null, null); + + KeyStore trustStore = null; + switch (secureConfig.getTrustAnchorOptions()) { + case USER_ONLY: + case USER_SYSTEM: + case LIMITED_SYSTEM: + trustStore = KeyStore.getInstance(secureConfig.getKeystoreType()); + trustStore.load(null, null); + break; + } + + switch (secureConfig.getTrustAnchorOptions()) { + case USER_SYSTEM: + KeyStore caStore = KeyStore.getInstance(secureConfig.getAndroidCAStore()); + caStore.load(null, null); + Enumeration caAliases = caStore.aliases(); + while (caAliases.hasMoreElements()) { + String alias = caAliases.nextElement(); + assert trustStore != null; + trustStore.setCertificateEntry(alias, caStore.getCertificate(alias)); + } + break; + case USER_ONLY: + case LIMITED_SYSTEM: + for (Map.Entry ca : trustAnchors.entrySet()) { + CertificateFactory cf = CertificateFactory.getInstance( + secureConfig.getCertPath()); + Certificate userCert = cf.generateCertificate(ca.getValue()); + assert trustStore != null; + trustStore.setCertificateEntry(ca.getKey(), userCert); + } + break; + } + + tmf.init(trustStore); + SSLContext sslContext = SSLContext.getInstance(SSL_TLS); + + KeyManager[] keyManagersArray = new KeyManager[1]; + keyManagersArray[0] = SecureKeyManager.getDefault( + secureURL.getClientCertAlias(), + secureConfig); + sslContext.init(keyManagersArray, tmf.getTrustManagers(), new SecureRandom()); + return sslContext.getSocketFactory(); + } catch (GeneralSecurityException | IOException ex) { + throw new SecurityException("Issue creating User SSLSocketFactory."); + } + } + +} diff --git a/niap-cc/NIAPSEC/src/main/res/values/strings.xml b/niap-cc/NIAPSEC/src/main/res/values/strings.xml new file mode 100644 index 0000000..b48bed4 --- /dev/null +++ b/niap-cc/NIAPSEC/src/main/res/values/strings.xml @@ -0,0 +1,17 @@ + + + + niapsec + diff --git a/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/SecureConfigTest.java b/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/SecureConfigTest.java new file mode 100644 index 0000000..3e0c4cc --- /dev/null +++ b/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/SecureConfigTest.java @@ -0,0 +1,82 @@ +package com.android.certifications.niap.niapsec; + +import android.security.keystore.KeyProperties; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + + +/** + * Unit tests for the {@link SecureConfig} class. + * These tests run on the local JVM using Robolectric. + */ +@RunWith(RobolectricTestRunner .class) +public class SecureConfigTest { + + @Test + public void getDefault_returnsNonNullConfig() { + // Act + SecureConfig config = SecureConfig.getDefault(); + + // Assert + org.junit.Assert.assertNotNull("The default SecureConfig should not be null.", config); + } + + @Test + public void getStrongConfig_hasCorrectDefaultValues() { + // Act + SecureConfig config = SecureConfig.getStrongConfig(); + + // Assert + org.junit.Assert.assertNotNull(config); + + // Verify some of the "strong" default values + org.junit.Assert.assertEquals("Asymmetric key algorithm should be RSA", + KeyProperties.KEY_ALGORITHM_RSA, config.getAsymmetricKeyPairAlgorithm()); + + org.junit.Assert.assertEquals("Asymmetric key size should be 3072", + 3072, config.getAsymmetricKeySize()); + + org.junit.Assert.assertEquals("Asymmetric padding should be OAEP", + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP, config.getAsymmetricPaddings()); + + org.junit.Assert.assertEquals("Symmetric key algorithm should be AES", + KeyProperties.KEY_ALGORITHM_AES, config.getSymmetricKeyAlgorithm()); + + org.junit.Assert.assertEquals("Symmetric block mode should be GCM", + KeyProperties.BLOCK_MODE_GCM, config.getSymmetricBlockModes()); + + org.junit.Assert.assertEquals("Symmetric key size should be 256", + 256, config.getSymmetricKeySize()); + + + } + + @Test + public void builder_canModifySymmetricKeySize() { + // Arrange + int customKeySize = 128; + + // Act + SecureConfig customConfig = new SecureConfig.Builder() + .setSymmetricKeySize(customKeySize) + .build(); + + // Assert + org.junit.Assert.assertEquals("Builder should correctly set the symmetric key size.", + customKeySize, customConfig.getSymmetricKeySize()); + } + + @Test + public void getStrongDeviceCredentialConfig_setsValiditySeconds() { + // Act + SecureConfig config = SecureConfig.getStrongDeviceCredentialConfig(null); + + // Assert + org.junit.Assert.assertEquals("Asymmetric auth validity should be 10 seconds", + 10, config.getAsymmetricRequireUserValiditySeconds()); + org.junit.Assert.assertEquals("Symmetric auth validity should be 10 seconds", + 10, config.getSymmetricRequireUserValiditySeconds()); + } +} \ No newline at end of file diff --git a/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactoryTest.java b/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactoryTest.java new file mode 100644 index 0000000..4dbd15e --- /dev/null +++ b/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactoryTest.java @@ -0,0 +1,92 @@ +package com.android.certifications.niap.niapsec.net; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; +import java.net.Socket; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +@RunWith(RobolectricTestRunner.class) +public class ValidatableSSLSocketFactoryTest { + + @Mock + private SecureURL mockSecureURL; + + @Mock + private SSLSocket mockSslSocket; + @Mock + private SSLSocketFactory mockSslSocketFactory; + + private SecureConfig secureConfig; + + + private ValidatableSSLSocketFactory factory; + + @Before + public void setUp() throws Exception { + // Initialize Mockito annotations + MockitoAnnotations.initMocks(this); + + // Use a real instance for SecureConfig + secureConfig = SecureConfig.getDefault(); + + // Configure the mock SecureURL + when(mockSecureURL.getHostname()).thenReturn("example.com"); + // Configure isValid to return true (for the valid certificate case) + when(mockSecureURL.isValid(anyString(), any(SSLSocket.class))).thenReturn(true); + + // Configure the mock SSLSocket + // Do nothing when startHandshake() is called + doNothing().when(mockSslSocket).startHandshake(); + + when(mockSslSocketFactory.getSupportedCipherSuites()).thenReturn(new String[0]); + when(mockSslSocketFactory.createSocket(any(Socket.class), anyString(), any(int.class), any(boolean.class))) + .thenReturn(mockSslSocket); + + // Initialize the factory + factory = new ValidatableSSLSocketFactory(mockSecureURL, mockSslSocketFactory,secureConfig); + } + + @Test + public void createSocket_returnsValidatableSSLSocketInstance() throws IOException { + // Act + // Call the factory's createSocket method, which internally calls + // new ValidatableSSLSocket() + Socket socket = factory.createSocket(mockSslSocket, "example.com", 443, true); + + // Assert + // Verify that the returned socket is not null + assertNotNull(socket); + // Verify that the returned socket is an instance of ValidatableSSLSocket + assertTrue(socket instanceof ValidatableSSLSocket); + } + + @Test(expected = IOException.class) + public void createSocket_withInvalidCertificate_throwsIOException() throws IOException { + // Arrange + // Configure SecureURL#isValid to return false (for the invalid certificate case) + when(mockSecureURL.isValid(anyString(), any(SSLSocket.class))).thenReturn(false); + + // Act & Assert + // Expect an IOException to be thrown from the constructor when createSocket is called, + // as it invokes isValid() + factory.createSocket(mockSslSocket, "example.com", 443, true); + } +} \ No newline at end of file diff --git a/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketTest.java b/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketTest.java new file mode 100644 index 0000000..a30ee14 --- /dev/null +++ b/niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketTest.java @@ -0,0 +1,124 @@ +package com.android.certifications.niap.niapsec.net; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.android.certifications.niap.niapsec.SecureConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; + +import javax.net.ssl.SSLSocket; + +@RunWith(RobolectricTestRunner.class) +public class ValidatableSSLSocketTest { + + @Mock + private SecureURL mockSecureURL; + + @Mock + private SSLSocket mockSslSocket; + + private SecureConfig secureConfig; + private ValidatableSSLSocket validatableSocket; + + // Cipher suites to use for testing + private final String[] strongCipherSuites = new String[]{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + // Configure the mock SecureURL + when(mockSecureURL.getHostname()).thenReturn("example.com"); + // Assume the certificate is valid by default + when(mockSecureURL.isValid(mockSecureURL.getHostname(), mockSslSocket)).thenReturn(true); + + // Configure the mock SSLSocket + doNothing().when(mockSslSocket).startHandshake(); + } + + @Test + public void constructor_whenStrongCiphersEnabled_setsEnabledCipherSuites() throws IOException { + // Arrange + // Configure to enable strong cipher suites + secureConfig = new SecureConfig.Builder(). + setUseStrongSSLCiphers(true).build(); + + // Act + validatableSocket = new ValidatableSSLSocket(mockSecureURL, mockSslSocket, secureConfig, strongCipherSuites); + + // Assert + // Verify that setEnabledCipherSuites was called with the specified cipher suites + verify(mockSslSocket, times(1)).setEnabledCipherSuites(strongCipherSuites); + } + + @Test + public void constructor_whenStrongCiphersDisabled_doesNotSetEnabledCipherSuites() throws IOException { + // Arrange + // Configure to disable strong cipher suites + secureConfig = new SecureConfig.Builder().setUseStrongSSLCiphers(false).build(); + + // Act + validatableSocket = new ValidatableSSLSocket(mockSecureURL, mockSslSocket, secureConfig, strongCipherSuites); + + // Assert + // Verify that setEnabledCipherSuites was not called + verify(mockSslSocket, times(0)).setEnabledCipherSuites(strongCipherSuites); + } + + @Test + public void constructor_performsHandshakeAndValidation() throws IOException { + // Arrange + secureConfig = SecureConfig.getDefault(); + + // Act + validatableSocket = new ValidatableSSLSocket(mockSecureURL, mockSslSocket, secureConfig, strongCipherSuites); + + // Assert + // Verify that startHandshake was called once + verify(mockSslSocket, times(1)).startHandshake(); + // Verify that SecureURL#isValid was called once + verify(mockSecureURL, times(1)).isValid(mockSecureURL.getHostname(), mockSslSocket); + } + + @Test + public void startHandshake_isIdempotent() throws IOException { + // Arrange + secureConfig = SecureConfig.getDefault(); + validatableSocket = new ValidatableSSLSocket(mockSecureURL, mockSslSocket, secureConfig, strongCipherSuites); + + // Act + // Call startHandshake multiple times + validatableSocket.startHandshake(); + validatableSocket.startHandshake(); + + // Assert + // The internal sslSocket.startHandshake() should only be called once in the constructor, + // because the handshakeStarted flag is set to true. Subsequent manual calls should do nothing. + verify(mockSslSocket, times(1)).startHandshake(); + } + + @Test + public void close_delegatesToWrappedSocket() throws IOException { + // Arrange + secureConfig = SecureConfig.getDefault(); + validatableSocket = new ValidatableSSLSocket(mockSecureURL, mockSslSocket, secureConfig, strongCipherSuites); + + // Act + validatableSocket.close(); + + // Assert + // Verify that the close() method of the wrapped mockSslSocket was called once + verify(mockSslSocket, times(1)).close(); + } +} \ No newline at end of file