From 03e6dd08e645cf927fc120dbf760e8202b2264f7 Mon Sep 17 00:00:00 2001 From: wkouki Date: Tue, 4 Nov 2025 16:43:09 +0900 Subject: [PATCH] Add NIAPSEC library to the repository. --- .idea/caches/deviceStreaming.xml | 981 +++++++++++ niap-cc/NIAPSEC/.gitignore | 35 + niap-cc/NIAPSEC/LICENSE | 201 +++ niap-cc/NIAPSEC/README.md | 26 + niap-cc/NIAPSEC/build.gradle | 101 ++ niap-cc/NIAPSEC/gradle.properties | 2 + .../NIAPSEC/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + niap-cc/NIAPSEC/gradlew | 172 ++ niap-cc/NIAPSEC/gradlew.bat | 84 + .../src/androidTest/AndroidManifest.xml | 19 + .../niap/niapsec/net/RealConnectionTest.java | 98 ++ niap-cc/NIAPSEC/src/main/AndroidManifest.xml | 18 + .../niap/niapsec/SecureConfig.java | 696 ++++++++ .../niapsec/biometric/BiometricSupport.java | 69 + .../biometric/BiometricSupportImpl.java | 188 ++ .../niap/niapsec/config/TldConstants.java | 1567 +++++++++++++++++ .../niapsec/config/TrustAnchorOptions.java | 52 + .../niapsec/context/SecureContextCompat.java | 145 ++ .../crypto/AuthenticatedFileCipher.java | 314 ++++ .../niapsec/crypto/EphemeralSecretKey.java | 329 ++++ .../niap/niapsec/crypto/FileCipher.java | 325 ++++ .../niap/niapsec/crypto/SecureCipher.java | 760 ++++++++ .../niapsec/crypto/SecureKeyGenerator.java | 160 ++ .../niap/niapsec/crypto/SecureKeyStore.java | 114 ++ .../niapsec/net/CertificateValidation.java | 26 + .../niap/niapsec/net/SecureKeyManager.java | 180 ++ .../niap/niapsec/net/SecureURL.java | 315 ++++ .../niapsec/net/ValidatableSSLSocket.java | 451 +++++ .../net/ValidatableSSLSocketFactory.java | 246 +++ .../NIAPSEC/src/main/res/values/strings.xml | 17 + .../niap/niapsec/SecureConfigTest.java | 82 + .../net/ValidatableSSLSocketFactoryTest.java | 92 + .../niapsec/net/ValidatableSSLSocketTest.java | 124 ++ 34 files changed, 7995 insertions(+) create mode 100644 .idea/caches/deviceStreaming.xml create mode 100644 niap-cc/NIAPSEC/.gitignore create mode 100644 niap-cc/NIAPSEC/LICENSE create mode 100644 niap-cc/NIAPSEC/README.md create mode 100644 niap-cc/NIAPSEC/build.gradle create mode 100644 niap-cc/NIAPSEC/gradle.properties create mode 100644 niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.jar create mode 100644 niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.properties create mode 100644 niap-cc/NIAPSEC/gradlew create mode 100644 niap-cc/NIAPSEC/gradlew.bat create mode 100644 niap-cc/NIAPSEC/src/androidTest/AndroidManifest.xml create mode 100644 niap-cc/NIAPSEC/src/androidTest/java/com/android/certifications/niap/niapsec/net/RealConnectionTest.java create mode 100644 niap-cc/NIAPSEC/src/main/AndroidManifest.xml create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/SecureConfig.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupport.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupportImpl.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TldConstants.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/config/TrustAnchorOptions.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/context/SecureContextCompat.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/AuthenticatedFileCipher.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/EphemeralSecretKey.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/FileCipher.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureCipher.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyGenerator.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyStore.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/CertificateValidation.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureKeyManager.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureURL.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocket.java create mode 100644 niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactory.java create mode 100644 niap-cc/NIAPSEC/src/main/res/values/strings.xml create mode 100644 niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/SecureConfigTest.java create mode 100644 niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketFactoryTest.java create mode 100644 niap-cc/NIAPSEC/src/test/java/com/android/certifications/niap/niapsec/net/ValidatableSSLSocketTest.java diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..34c77d88 --- /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 00000000..8d49328d --- /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 00000000..261eeb9e --- /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 00000000..54ce91f4 --- /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 00000000..89cee70b --- /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 00000000..646c51b9 --- /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 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 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 00000000..61e81110 --- /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 00000000..cccdd3d5 --- /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 00000000..f9553162 --- /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 00000000..859c04f3 --- /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 00000000..6ccb9d91 --- /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 00000000..be0f4a20 --- /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 00000000..05646579 --- /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 00000000..342c50ce --- /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 00000000..a4ec0a24 --- /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 00000000..10de4865 --- /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 00000000..6b2b8b87 --- /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 00000000..f9bb6d41 --- /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 00000000..90bb82e0 --- /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 00000000..7ae074ef --- /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 00000000..89f090ae --- /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 00000000..a2a20b8a --- /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 00000000..09aa5cfd --- /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 00000000..103923e9 --- /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 00000000..d808656f --- /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 00000000..7abf24ea --- /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 00000000..64c9c577 --- /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 00000000..e71493fb --- /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 00000000..420523e0 --- /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 00000000..b48bed4d --- /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 00000000..3e0c4cc1 --- /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 00000000..4dbd15e8 --- /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 00000000..a30ee149 --- /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