diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
new file mode 100644
index 0000000..34c77d8
--- /dev/null
+++ b/.idea/caches/deviceStreaming.xml
@@ -0,0 +1,981 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/niap-cc/NIAPSEC/.gitignore b/niap-cc/NIAPSEC/.gitignore
new file mode 100644
index 0000000..8d49328
--- /dev/null
+++ b/niap-cc/NIAPSEC/.gitignore
@@ -0,0 +1,35 @@
+# Android generated
+bin
+gen
+libs
+obj
+lint.xml
+
+# Android Studio
+.idea
+.idea/
+*.iml
+*.ipr
+*.iws
+classes
+gen-external-apklibs
+
+# Gradle
+.gradle
+build
+buildout
+out
+
+# Maven
+target
+release.properties
+pom.xml.*
+
+local.properties
+proguard-rules.pro
+
+# Other
+.DS_Store
+dist
+tmp/build
+
diff --git a/niap-cc/NIAPSEC/LICENSE b/niap-cc/NIAPSEC/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/niap-cc/NIAPSEC/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/niap-cc/NIAPSEC/README.md b/niap-cc/NIAPSEC/README.md
new file mode 100644
index 0000000..54ce91f
--- /dev/null
+++ b/niap-cc/NIAPSEC/README.md
@@ -0,0 +1,26 @@
+NIAPSEC - Security Extensions for NIAP on Android
+========================
+NIAPSEC aims to provide an easy to use module that provides NIAP compliant functionality for key management, TLS, OCSP, and more.
+
+Experimental Release
+
+License
+-------
+
+Copyright 2018 Google, Inc.
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements. See the NOTICE file distributed with this work for
+additional information regarding copyright ownership. The ASF licenses this
+file to you under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+
diff --git a/niap-cc/NIAPSEC/build.gradle b/niap-cc/NIAPSEC/build.gradle
new file mode 100644
index 0000000..89cee70
--- /dev/null
+++ b/niap-cc/NIAPSEC/build.gradle
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+buildscript {
+
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:8.8.2"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://jitpack.io" }
+ }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+ namespace="com.android.certifications.niap.niapsec"
+ compileSdk 36
+ defaultConfig {
+ minSdkVersion 26
+ //For robolectric test
+ //noinspection OldTargetApi
+ targetSdkVersion 34
+ versionCode 3
+ versionName "3.0.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility = '1.17'
+ targetCompatibility = '1.17'
+ }
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+ sourceSets {
+ main {
+ manifest.srcFile 'src/main/AndroidManifest.xml'
+ }
+ androidTest {
+ manifest.srcFile 'src/androidTest/AndroidManifest.xml'
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation 'androidx.appcompat:appcompat:1.7.1'
+ implementation 'androidx.biometric:biometric:1.1.0'
+ implementation 'org.conscrypt:conscrypt-android:2.5.2'
+ implementation 'com.github.ChickenHook:RestrictionBypass:2.2'
+
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'androidx.test:core:1.7.0'
+ testImplementation 'androidx.test.ext:junit:1.3.0' // AndroidX extensions for JUnit
+ testImplementation 'org.robolectric:robolectric:4.13'
+
+ testImplementation 'org.mockito:mockito-core:5.12.0'
+
+ // For instrumented tests (run on an Android device/emulator)
+ androidTestImplementation 'androidx.test.ext:junit:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
+ // Note: The original android.support.test.runner is outdated.
+ // The line below is the modern replacement.
+ androidTestImplementation 'androidx.test:runner:1.7.0'
+
+}
diff --git a/niap-cc/NIAPSEC/gradle.properties b/niap-cc/NIAPSEC/gradle.properties
new file mode 100644
index 0000000..646c51b
--- /dev/null
+++ b/niap-cc/NIAPSEC/gradle.properties
@@ -0,0 +1,2 @@
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.jar b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.properties b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..61e8111
--- /dev/null
+++ b/niap-cc/NIAPSEC/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Oct 31 10:55:08 JST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/niap-cc/NIAPSEC/gradlew b/niap-cc/NIAPSEC/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/niap-cc/NIAPSEC/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/niap-cc/NIAPSEC/gradlew.bat b/niap-cc/NIAPSEC/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/niap-cc/NIAPSEC/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/niap-cc/NIAPSEC/src/androidTest/AndroidManifest.xml b/niap-cc/NIAPSEC/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..859c04f
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/niap-cc/NIAPSEC/src/androidTest/java/com/android/certifications/niap/niapsec/net/RealConnectionTest.java b/niap-cc/NIAPSEC/src/androidTest/java/com/android/certifications/niap/niapsec/net/RealConnectionTest.java
new file mode 100644
index 0000000..6ccb9d9
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/androidTest/java/com/android/certifications/niap/niapsec/net/RealConnectionTest.java
@@ -0,0 +1,98 @@
+package com.android.certifications.niap.niapsec.net;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.certifications.niap.niapsec.SecureConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Instrumented test for real network connections using ValidatableSSLSocketFactory.
+ * This test requires an active internet connection and runs on an Android device or emulator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RealConnectionTest {
+
+ private Context context;
+ private SecureConfig secureConfig;
+
+ @Before
+ public void setUp() {
+ // Get the context of the app under test.
+ context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ // Use the default secure configuration.
+ secureConfig = SecureConfig.getDefault();
+ }
+
+ /**
+ * Tests a successful connection to a trusted endpoint (google.com).
+ * The TLS handshake and certificate validation should complete without errors.
+ */
+ @Test
+ public void connectToTrustedServer_shouldSucceed() {
+ HttpsURLConnection urlConnection = null;
+
+ //We can use badssl.com for testing ssl https://badssl.com/
+ //String trustedHost = "https://tls-v1-2.badssl.com:1012/";
+ //String trustedHost = "https://www.google.com";
+ //String trustedHost = "https://wikipedia.org";
+ //revoked.grc.com
+ //String trustedHost = "https://revoked.grc.com";
+ //String trustedHost = "https://www.grc.com";
+ String trustedHost = "https://www.netscaler.com:443";
+ try {
+ // 1. Create a SecureURL instance for a trusted site.
+ SecureURL secureURL = new SecureURL(trustedHost,null ,secureConfig);
+
+ // 2. Create the custom SSLSocketFactory.
+ // This factory will perform the certificate validation.
+ SSLSocketFactory factory = new ValidatableSSLSocketFactory(secureURL);
+
+ // 3. Create a URL object and open a standard HttpsURLConnection.
+ URL url = new URL(trustedHost);
+ urlConnection = (HttpsURLConnection) url.openConnection();
+
+ // 4. Set our custom factory on the connection. This is the key step.
+ urlConnection.setSSLSocketFactory(factory);
+
+ // 5. Connect and get the response code. This triggers the handshake via our factory.
+ int responseCode = urlConnection.getResponseCode();
+
+ // 6. Read some data to ensure the connection is fully established.
+ InputStream in = urlConnection.getInputStream();
+ byte[] buffer = new byte[1024];
+ int bytesRead = in.read(buffer);
+ in.close();
+
+ // Assert that the connection was successful.
+ System.out.println("Successfully connected to google.com with response code: " + responseCode);
+ System.out.println("Read " + bytesRead + " bytes.");
+
+ assertNotNull(in);
+
+ } catch (IOException e) {
+ // If any IOException occurs, the test fails.
+ e.printStackTrace();
+ fail("Connection to trusted server failed with IOException: " + e.getMessage());
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/niap-cc/NIAPSEC/src/main/AndroidManifest.xml b/niap-cc/NIAPSEC/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..be0f4a2
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/SecureConfig.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/SecureConfig.java
new file mode 100644
index 0000000..0564657
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/SecureConfig.java
@@ -0,0 +1,696 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec;
+
+import android.security.keystore.KeyProperties;
+
+import com.android.certifications.niap.niapsec.biometric.BiometricSupport;
+import com.android.certifications.niap.niapsec.config.TrustAnchorOptions;
+
+/**
+ * A global configuration object that shows configuration options for testing scenarios.
+ *
+ */
+public class SecureConfig {
+
+ public static final String ANDROID_KEYSTORE = "AndroidKeyStore";
+ public static final String ANDROID_CA_STORE = "AndroidCAStore";
+ public static final int AES_IV_SIZE_BYTES = 12;
+ public static final String SSL_TLS = "TLS";
+ public static String PACKAGE_NAME = "com.android.certifications.niap.niapsec";
+
+ private String androidKeyStore = SecureConfig.ANDROID_KEYSTORE;
+ private String androidCAStore = SecureConfig.ANDROID_CA_STORE;
+ private String keystoreType = "PKCS12";
+
+ // Asymmetric Encryption Constants
+ private String asymmetricKeyPairAlgorithm = KeyProperties.KEY_ALGORITHM_RSA;
+ private int asymmetricKeySize = 3072;
+ private int teeGCIterations = 10;
+ //private String asymmetricCipherTransformation = "RSA/ECB/PKCS1Padding";
+ private String asymmetricCipherTransformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
+ private String asymmetricBlockModes = KeyProperties.BLOCK_MODE_ECB;
+ //private String asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1;
+ private String asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_OAEP;
+ private int asymmetricKeyPurposes = KeyProperties.PURPOSE_DECRYPT;
+ // Sets KeyGenBuilder#setUnlockedDeviceRequired to true, requires Android 9 Pie.
+ private boolean asymmetricSensitiveDataProtection = true;
+ private boolean asymmetricRequireUserAuth = true;
+ private int asymmetricRequireUserValiditySeconds = -1;
+ private String asymmetricDigests = KeyProperties.DIGEST_SHA256;
+
+ // Symmetric Encryption Constants
+ private String symmetricKeyAlgorithm = KeyProperties.KEY_ALGORITHM_AES;
+ private String symmetricBlockModes = KeyProperties.BLOCK_MODE_GCM;
+ private String symmetricPaddings = KeyProperties.ENCRYPTION_PADDING_NONE;
+ private int symmetricKeySize = 256;
+ private int symmetricGcmTagLength = 128;
+ private int symmetricKeyPurposes =
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT;
+ private String symmetricCipherTransformation = "AES/GCM/NoPadding";
+ // Sets KeyGenBuilder#setUnlockedDeviceRequired to true, requires Android 9 Pie.
+ private boolean symmetricSensitiveDataProtection = true;
+ private boolean symmetricRequireUserAuth = true;
+ private int symmetricRequireUserValiditySeconds = -1;
+ private String symmetricDigests = null;
+
+ private boolean logging = false;
+
+ // Certificate Constants
+ private String certPath = "X.509";
+ private String certPathValidator = "PKIX";
+ private boolean useStrongSSLCiphers = true;
+ private String[] strongSSLCiphers = new String[]{
+ "TLS_RSA_WITH_AES_128_CBC_SHA256",
+ "TLS_RSA_WITH_AES_256_CBC_SHA256",
+ "TLS_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
+ };
+ private String[] clientCertAlgorithms = new String[]{"RSA"};
+ private TrustAnchorOptions trustAnchorOptions = TrustAnchorOptions.USER_SYSTEM;
+
+ private BiometricSupport biometricSupport = null;
+
+ private SecureConfig() {
+ }
+
+ /**
+ * Builder for SecureConfig
+ */
+ public static class Builder {
+
+ public Builder() {
+ }
+
+ // Keystore Constants
+ private String androidKeyStore;
+ private String androidCAStore;
+ private String keystoreType;
+
+ public Builder forKeyStoreType(String keystoreType) {
+ this.keystoreType = keystoreType;
+ return this;
+ }
+
+ // Asymmetric Encryption Constants
+ private int teeGCIterations;
+ private String asymmetricKeyPairAlgorithm;
+ private int asymmetricKeySize;
+ private String asymmetricCipherTransformation;
+ private int asymmetricKeyPurposes;
+ private String asymmetricBlockModes;
+ private String asymmetricPaddings;
+ private boolean asymmetricSensitiveDataProtection;
+ private boolean asymmetricRequireUserAuth = true;
+ private int asymmetricRequireUserValiditySeconds = -1;
+
+ public Builder setAsymmetricKeyPairAlgorithm(String keyPairAlgorithm) {
+ this.asymmetricKeyPairAlgorithm = keyPairAlgorithm;
+ return this;
+ }
+
+ public Builder setAsymmetricKeySize(int keySize) {
+ this.asymmetricKeySize = keySize;
+ return this;
+ }
+
+ public Builder setTeeGCIterations(int iterations) {
+ this.teeGCIterations = iterations;
+ return this;
+ }
+
+ public Builder setAsymmetricCipherTransformation(String cipherTransformation) {
+ this.asymmetricCipherTransformation = cipherTransformation;
+ return this;
+ }
+
+ public Builder setAsymmetricKeyPurposes(int purposes) {
+ this.asymmetricKeyPurposes = purposes;
+ return this;
+ }
+
+ public Builder setAsymmetricBlockModes(String blockModes) {
+ this.asymmetricBlockModes = blockModes;
+ return this;
+ }
+
+ public Builder setAsymmetricPaddings(String paddings) {
+ this.asymmetricPaddings = paddings;
+ return this;
+ }
+
+ public Builder setAsymmetricSensitiveDataProtection(boolean dataProtection) {
+ this.asymmetricSensitiveDataProtection = dataProtection;
+ return this;
+ }
+
+ public Builder setAsymmetricRequireUserAuth(boolean userAuth) {
+ this.asymmetricRequireUserAuth = userAuth;
+ return this;
+ }
+
+ public Builder setAsymmetricRequireUserValiditySeconds(int authValiditySeconds) {
+ this.asymmetricRequireUserValiditySeconds = authValiditySeconds;
+ return this;
+ }
+
+ // Symmetric Encryption Constants
+ private String symmetricKeyAlgorithm;
+ private String symmetricBlockModes;
+ private String symmetricPaddings;
+ private int symmetricKeySize;
+ private int symmetricGcmTagLength;
+ private int symmetricKeyPurposes;
+ private String symmetricCipherTransformation;
+ private boolean symmetricSensitiveDataProtection;
+ private boolean symmetricRequireUserAuth = true;
+ private int symmetricRequireUserValiditySeconds = -1;
+
+ private boolean logging = false;
+
+ public Builder setLoggingEnabled(boolean enabled) {
+ logging = enabled;
+ return this;
+ }
+
+ public Builder setSymmetricKeyAlgorithm(String keyAlgorithm) {
+ this.symmetricKeyAlgorithm = keyAlgorithm;
+ return this;
+ }
+
+ public Builder setSymmetricKeySize(int keySize) {
+ this.symmetricKeySize = keySize;
+ return this;
+ }
+
+ public Builder setSymmetricCipherTransformation(String cipherTransformation) {
+ this.symmetricCipherTransformation = cipherTransformation;
+ return this;
+ }
+
+ public Builder setSymmetricKeyPurposes(int purposes) {
+ this.symmetricKeyPurposes = purposes;
+ return this;
+ }
+
+ public Builder setSymmetricGcmTagLength(int gcmTagLength) {
+ this.symmetricGcmTagLength = gcmTagLength;
+ return this;
+ }
+
+ public Builder setSymmetricBlockModes(String blockModes) {
+ this.symmetricBlockModes = blockModes;
+ return this;
+ }
+
+ public Builder setSymmetricPaddings(String paddings) {
+ this.symmetricPaddings = paddings;
+ return this;
+ }
+
+ public Builder setSymmetricSensitiveDataProtection(boolean dataProtection) {
+ this.symmetricSensitiveDataProtection = dataProtection;
+ return this;
+ }
+
+ public Builder setSymmetricRequireUserAuth(boolean userAuth) {
+ this.symmetricRequireUserAuth = userAuth;
+ return this;
+ }
+
+ public Builder setSymmetricRequireUserValiditySeconds(int authValiditySeconds) {
+ this.symmetricRequireUserValiditySeconds = authValiditySeconds;
+ return this;
+ }
+
+ // Certificate Constants
+ private String certPath;
+ private String certPathValidator;
+ private boolean useStrongSSLCiphers;
+ private String[] strongSSLCiphers;
+ private String[] clientCertAlgorithms;
+ private TrustAnchorOptions trustAnchorOptions;
+ private BiometricSupport biometricSupport;
+
+ public Builder setCertPath(String certPath) {
+ this.certPath = certPath;
+ return this;
+ }
+
+ public Builder setCertPathValidator(String certPathValidator) {
+ this.certPathValidator = certPathValidator;
+ return this;
+ }
+
+ public Builder setUseStrongSSLCiphers(boolean strongSSLCiphers) {
+ this.useStrongSSLCiphers = strongSSLCiphers;
+ return this;
+ }
+
+ public Builder setStrongSSLCiphers(String[] strongSSLCiphers) {
+ this.strongSSLCiphers = strongSSLCiphers;
+ return this;
+ }
+
+ public Builder setClientCertAlgorithms(String[] clientCertAlgorithms) {
+ this.clientCertAlgorithms = clientCertAlgorithms;
+ return this;
+ }
+
+ public Builder setTrustAnchorOptions(TrustAnchorOptions trustAnchorOptions) {
+ this.trustAnchorOptions = trustAnchorOptions;
+ return this;
+ }
+
+ public Builder setBiometricSupport(BiometricSupport biometricSupport) {
+ this.biometricSupport = biometricSupport;
+ return this;
+ }
+
+ public SecureConfig build() {
+ SecureConfig secureConfig = new SecureConfig();
+ secureConfig.androidKeyStore = this.androidKeyStore;
+ secureConfig.androidCAStore = this.androidCAStore;
+ secureConfig.keystoreType = this.keystoreType;
+
+ secureConfig.asymmetricKeyPairAlgorithm = this.asymmetricKeyPairAlgorithm;
+ secureConfig.asymmetricKeySize = this.asymmetricKeySize;
+ secureConfig.teeGCIterations = this.teeGCIterations;
+ secureConfig.asymmetricCipherTransformation = this.asymmetricCipherTransformation;
+ secureConfig.asymmetricKeyPurposes = this.asymmetricKeyPurposes;
+ secureConfig.asymmetricBlockModes = this.asymmetricBlockModes;
+ secureConfig.asymmetricPaddings = this.asymmetricPaddings;
+ secureConfig.asymmetricSensitiveDataProtection =
+ this.asymmetricSensitiveDataProtection;
+ secureConfig.asymmetricRequireUserAuth = this.asymmetricRequireUserAuth;
+ secureConfig.asymmetricRequireUserValiditySeconds =
+ this.asymmetricRequireUserValiditySeconds;
+
+ secureConfig.symmetricKeyAlgorithm = this.symmetricKeyAlgorithm;
+ secureConfig.symmetricBlockModes = this.symmetricBlockModes;
+ secureConfig.symmetricPaddings = this.symmetricPaddings;
+ secureConfig.symmetricKeySize = this.symmetricKeySize;
+ secureConfig.symmetricGcmTagLength = this.symmetricGcmTagLength;
+ secureConfig.symmetricKeyPurposes = this.symmetricKeyPurposes;
+ secureConfig.symmetricCipherTransformation = this.symmetricCipherTransformation;
+ secureConfig.symmetricSensitiveDataProtection = this.symmetricSensitiveDataProtection;
+ secureConfig.symmetricRequireUserAuth = this.symmetricRequireUserAuth;
+ secureConfig.symmetricRequireUserValiditySeconds =
+ this.symmetricRequireUserValiditySeconds;
+
+ secureConfig.certPath = this.certPath;
+ secureConfig.certPathValidator = this.certPathValidator;
+ secureConfig.useStrongSSLCiphers = this.useStrongSSLCiphers;
+ secureConfig.strongSSLCiphers = this.strongSSLCiphers;
+ secureConfig.clientCertAlgorithms = this.clientCertAlgorithms;
+ secureConfig.trustAnchorOptions = this.trustAnchorOptions;
+ secureConfig.biometricSupport = this.biometricSupport;
+ secureConfig.logging = this.logging;
+
+ return secureConfig;
+ }
+ }
+
+ /**
+ * Gets the default strong config
+ *
+ * @return The default config without biometric support with recommended NIAP settings
+ */
+ public static SecureConfig getDefault() {
+ return getStrongConfig(null);
+ }
+
+ /**
+ * Gets the default strong config with biometric support
+ *
+ * @param biometricSupport The callbacks
+ * @return A config with biometric support configured
+ */
+ public static SecureConfig getDefault(BiometricSupport biometricSupport) {
+ return getStrongConfig(biometricSupport);
+ }
+
+ /**
+ * Gets the default strong config
+ *
+ * @return Default config with the strongest encryption settings
+ */
+ public static SecureConfig getStrongConfig() {
+ return getStrongConfig(null);
+ }
+
+ /**
+ * Gets the default strong config with biometric support
+ *
+ * @param biometricSupport The biometric callback used for device credential
+ * @return A secure config with strong NIAP settings built in and biometric support
+ */
+ public static SecureConfig getStrongDeviceCredentialConfig(BiometricSupport biometricSupport) {
+ SecureConfig secureConfig = getStrongConfig(biometricSupport);
+ secureConfig.asymmetricRequireUserValiditySeconds = 10;
+ secureConfig.symmetricRequireUserValiditySeconds = 10;
+ return secureConfig;
+ }
+
+ /**
+ * Gets the default strong config with biometric support
+ *
+ * @param biometricSupport The biometric callback
+ * @return A secure config with strong NIAP settings built in and biometric support
+ */
+ public static SecureConfig getStrongConfig(BiometricSupport biometricSupport) {
+ SecureConfig.Builder builder = new SecureConfig.Builder();
+ builder.androidKeyStore = SecureConfig.ANDROID_KEYSTORE;
+ builder.androidCAStore = SecureConfig.ANDROID_CA_STORE;
+ builder.keystoreType = "PKCS12";
+
+ builder.asymmetricKeyPairAlgorithm = KeyProperties.KEY_ALGORITHM_RSA;
+ builder.asymmetricKeySize = 3072;
+ builder.teeGCIterations = 10;
+ //builder.asymmetricCipherTransformation = "RSA/ECB/PKCS1Padding";
+ builder.asymmetricCipherTransformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
+ builder.asymmetricBlockModes = KeyProperties.BLOCK_MODE_ECB;
+ //builder.asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1;
+ builder.asymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_OAEP;
+ builder.asymmetricKeyPurposes = KeyProperties.PURPOSE_DECRYPT;
+ builder.asymmetricSensitiveDataProtection = true;
+ builder.asymmetricRequireUserAuth = true;
+ builder.asymmetricRequireUserValiditySeconds = -1;
+
+ builder.symmetricKeyAlgorithm = KeyProperties.KEY_ALGORITHM_AES;
+ builder.symmetricBlockModes = KeyProperties.BLOCK_MODE_GCM;
+ builder.symmetricPaddings = KeyProperties.ENCRYPTION_PADDING_NONE;
+ builder.symmetricKeySize = 256;
+ builder.symmetricGcmTagLength = 128;
+ builder.symmetricKeyPurposes =
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT;
+ builder.symmetricCipherTransformation = "AES/GCM/NoPadding";
+ builder.symmetricSensitiveDataProtection = true;
+ builder.symmetricRequireUserAuth = true;
+ builder.symmetricRequireUserValiditySeconds = -1;
+
+ builder.certPath = "X.509";
+ builder.certPathValidator = "PKIX";
+ builder.useStrongSSLCiphers = true;
+ builder.strongSSLCiphers = new String[]{
+ "TLS_RSA_WITH_AES_128_CBC_SHA256",
+ "TLS_RSA_WITH_AES_256_CBC_SHA256",
+ "TLS_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
+ };
+ builder.clientCertAlgorithms = new String[]{"RSA"};
+ builder.trustAnchorOptions = TrustAnchorOptions.USER_SYSTEM;
+ builder.biometricSupport = biometricSupport;
+
+ return builder.build();
+ }
+
+ /**
+ * @return The android key store
+ */
+ public String getAndroidKeyStore() {
+ return androidKeyStore;
+ }
+
+ public void setAndroidKeyStore(String androidKeyStore) {
+ this.androidKeyStore = androidKeyStore;
+ }
+
+ /**
+ * @return The Android CA store
+ */
+ public String getAndroidCAStore() {
+ return androidCAStore;
+ }
+
+ public void setAndroidCAStore(String androidCAStore) {
+ this.androidCAStore = androidCAStore;
+ }
+
+ public String getKeystoreType() {
+ return keystoreType;
+ }
+
+ public void setKeystoreType(String keystoreType) {
+ this.keystoreType = keystoreType;
+ }
+
+ public String getAsymmetricKeyPairAlgorithm() {
+ return asymmetricKeyPairAlgorithm;
+ }
+
+ public void setAsymmetricKeyPairAlgorithm(String asymmetricKeyPairAlgorithm) {
+ this.asymmetricKeyPairAlgorithm = asymmetricKeyPairAlgorithm;
+ }
+
+ public int getAsymmetricKeySize() {
+ return asymmetricKeySize;
+ }
+
+ public void setAsymmetricKeySize(int asymmetricKeySize) {
+ this.asymmetricKeySize = asymmetricKeySize;
+ }
+
+ public String getAsymmetricCipherTransformation() {
+ return asymmetricCipherTransformation;
+ }
+
+ public void setAsymmetricCipherTransformation(String asymmetricCipherTransformation) {
+ this.asymmetricCipherTransformation = asymmetricCipherTransformation;
+ }
+
+ public String getAsymmetricBlockModes() {
+ return asymmetricBlockModes;
+ }
+
+ public void setAsymmetricBlockModes(String asymmetricBlockModes) {
+ this.asymmetricBlockModes = asymmetricBlockModes;
+ }
+
+ public String getAsymmetricPaddings() {
+ return asymmetricPaddings;
+ }
+
+ public void setAsymmetricPaddings(String asymmetricPaddings) {
+ this.asymmetricPaddings = asymmetricPaddings;
+ }
+
+ public int getAsymmetricKeyPurposes() {
+ return asymmetricKeyPurposes;
+ }
+
+ public void setAsymmetricKeyPurposes(int asymmetricKeyPurposes) {
+ this.asymmetricKeyPurposes = asymmetricKeyPurposes;
+ }
+
+ public boolean isAsymmetricSensitiveDataProtectionEnabled() {
+ return asymmetricSensitiveDataProtection;
+ }
+
+ public void setAsymmetricSensitiveDataProtection(boolean asymmetricSensitiveDataProtection) {
+ this.asymmetricSensitiveDataProtection = asymmetricSensitiveDataProtection;
+ }
+
+ public boolean isAsymmetricRequireUserAuthEnabled() {
+ return asymmetricRequireUserAuth && biometricSupport != null;
+ }
+
+ public void setAsymmetricRequireUserAuth(boolean requireUserAuth) {
+ this.asymmetricRequireUserAuth = requireUserAuth;
+ }
+
+ public int getAsymmetricRequireUserValiditySeconds() {
+ return this.asymmetricRequireUserValiditySeconds;
+ }
+
+ public void setAsymmetricRequireUserValiditySeconds(int userValiditySeconds) {
+ this.asymmetricRequireUserValiditySeconds = userValiditySeconds;
+ }
+
+
+ public String getSymmetricKeyAlgorithm() {
+ return symmetricKeyAlgorithm;
+ }
+
+ public void setSymmetricKeyAlgorithm(String symmetricKeyAlgorithm) {
+ this.symmetricKeyAlgorithm = symmetricKeyAlgorithm;
+ }
+
+ public String getSymmetricBlockModes() {
+ return symmetricBlockModes;
+ }
+
+ public void setSymmetricBlockModes(String symmetricBlockModes) {
+ this.symmetricBlockModes = symmetricBlockModes;
+ }
+
+ public String getSymmetricPaddings() {
+ return symmetricPaddings;
+ }
+
+ public void setSymmetricPaddings(String symmetricPaddings) {
+ this.symmetricPaddings = symmetricPaddings;
+ }
+
+ public int getSymmetricKeySize() {
+ return symmetricKeySize;
+ }
+
+ public void setSymmetricKeySize(int symmetricKeySize) {
+ this.symmetricKeySize = symmetricKeySize;
+ }
+
+ public int getSymmetricGcmTagLength() {
+ return symmetricGcmTagLength;
+ }
+
+ public void setSymmetricGcmTagLength(int symmetricGcmTagLength) {
+ this.symmetricGcmTagLength = symmetricGcmTagLength;
+ }
+
+ public int getSymmetricKeyPurposes() {
+ return symmetricKeyPurposes;
+ }
+
+ public void setSymmetricKeyPurposes(int symmetricKeyPurposes) {
+ this.symmetricKeyPurposes = symmetricKeyPurposes;
+ }
+
+ public String getSymmetricCipherTransformation() {
+ return symmetricCipherTransformation;
+ }
+
+ public void setSymmetricCipherTransformation(String symmetricCipherTransformation) {
+ this.symmetricCipherTransformation = symmetricCipherTransformation;
+ }
+
+ public boolean isSymmetricSensitiveDataProtectionEnabled() {
+ return symmetricSensitiveDataProtection;
+ }
+
+ public void setSymmetricSensitiveDataProtection(boolean symmetricSensitiveDataProtection) {
+ this.symmetricSensitiveDataProtection = symmetricSensitiveDataProtection;
+ }
+
+ public boolean isSymmetricRequireUserAuthEnabled() {
+ return symmetricRequireUserAuth;
+ }
+
+ public void setSymmetricRequireUserAuth(boolean requireUserAuth) {
+ this.symmetricRequireUserAuth = requireUserAuth;
+ }
+
+ public int getSymmetricRequireUserValiditySeconds() {
+ return this.symmetricRequireUserValiditySeconds;
+ }
+
+ public void setSymmetricRequireUserValiditySeconds(int userValiditySeconds) {
+ this.symmetricRequireUserValiditySeconds = userValiditySeconds;
+ }
+
+ public String getCertPath() {
+ return certPath;
+ }
+
+ public void setCertPath(String certPath) {
+ this.certPath = certPath;
+ }
+
+ public String getCertPathValidator() {
+ return certPathValidator;
+ }
+
+ public void setCertPathValidator(String certPathValidator) {
+ this.certPathValidator = certPathValidator;
+ }
+
+ public boolean isUseStrongSSLCiphersEnabled() {
+ return useStrongSSLCiphers;
+ }
+
+ public void setUseStrongSSLCiphers(boolean useStrongSSLCiphers) {
+ this.useStrongSSLCiphers = useStrongSSLCiphers;
+ }
+
+ public boolean isUseStrongSSLCiphers() {
+ return useStrongSSLCiphers;
+ }
+
+ public String[] getStrongSSLCiphers() {
+ return strongSSLCiphers;
+ }
+
+ public void setStrongSSLCiphers(String[] strongSSLCiphers) {
+ this.strongSSLCiphers = strongSSLCiphers;
+ }
+
+ public String[] getClientCertAlgorithms() {
+ return clientCertAlgorithms;
+ }
+
+ public void setClientCertAlgorithms(String[] clientCertAlgorithms) {
+ this.clientCertAlgorithms = clientCertAlgorithms;
+ }
+
+ public TrustAnchorOptions getTrustAnchorOptions() {
+ return trustAnchorOptions;
+ }
+
+ public void setTrustAnchorOptions(TrustAnchorOptions trustAnchorOptions) {
+ this.trustAnchorOptions = trustAnchorOptions;
+ }
+
+ public BiometricSupport getBiometricSupport() {
+ return biometricSupport;
+ }
+
+ public void setBiometricSupport(BiometricSupport biometricSupport) {
+ this.biometricSupport = biometricSupport;
+ }
+
+ public void setDebugLoggingEnabled(boolean enabled) {
+ logging = enabled;
+ }
+
+ public boolean isDebugLoggingEnabled() {
+ return logging;
+ }
+
+ public int getTeeGCIterations() {
+ return teeGCIterations;
+ }
+
+ public void setTeeGCIterations(int iterations) {
+ teeGCIterations = iterations;
+ }
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupport.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupport.java
new file mode 100644
index 0000000..342c50c
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupport.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.certifications.niap.niapsec.biometric;
+
+import javax.crypto.Cipher;
+
+import com.android.certifications.niap.niapsec.crypto.SecureCipher;
+
+/**
+ * Interface to define various events and enums used by BiometricPrompt.
+ *
+ */
+public interface BiometricSupport {
+
+ /**
+ * Statuses of biometric authentication
+ */
+ public enum BiometricStatus {
+ SUCCESS(0),
+ FAILED(1),
+ CANCELLED(2);
+
+ private final int type;
+
+ BiometricStatus(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public static BiometricStatus fromId(int id) {
+ switch (id) {
+ case 0:
+ return SUCCESS;
+ case 1:
+ return FAILED;
+ case 2:
+ return CANCELLED;
+ }
+ return CANCELLED;
+ }
+ }
+
+ void onAuthenticationSucceeded();
+
+ void onAuthenticationFailed();
+
+ void onMessage(String message);
+
+ void authenticate(Cipher cipher, SecureCipher.SecureAuthCallback callback);
+
+ void authenticateDeviceCredential(SecureCipher.SecureAuthCallback callback);
+
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupportImpl.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupportImpl.java
new file mode 100644
index 0000000..a4ec0a2
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/biometric/BiometricSupportImpl.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec.biometric;
+
+import android.content.Context;
+
+
+import android.util.Log;
+
+import com.android.certifications.niap.niapsec.crypto.SecureCipher;
+
+import java.util.concurrent.Executor;
+
+import javax.crypto.Cipher;
+
+import androidx.biometric.BiometricPrompt;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Implementation of Biometric support that handles callbacks and showing the UI elements for
+ * key use authorization for the AndroidKeyStore.
+ */
+public abstract class BiometricSupportImpl extends BiometricPrompt.AuthenticationCallback
+ implements BiometricSupport {
+
+ private static final String TAG = "BiometricSupportImpl";
+
+ private FragmentActivity activity;
+ private Context context;
+ private boolean useDeviceCredential = false;
+ private SecureCipher.SecureAuthCallback secureAuthCallback;
+ private static final String keyName = "biometric_key";
+ private BiometricPrompt biometricPrompt;
+ private Executor executor;
+
+ /**
+ * Create a Biometric support object with settings from the calling app
+ *
+ * @param activity The activity of the calling app
+ * @param context The context of the calling app
+ * @param useDeviceCredential true if testing on a device without biometrics and fingerprint
+ * support. false to test with the biometrics.
+ *
+ * NOTE: If useDeviceCredential is true, you must set your config
+ * to allow for a time-bound key approach in the android keystore.
+ * userValiditySeconds must be > 0
+ */
+ public BiometricSupportImpl(FragmentActivity activity,
+ Context context,
+ boolean useDeviceCredential) {
+ this.activity = activity;
+ this.context = context;
+ this.useDeviceCredential = useDeviceCredential;
+ this.executor = ContextCompat.getMainExecutor(activity);
+ this.biometricPrompt = new BiometricPrompt(activity, executor, this);
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
+ super.onAuthenticationSucceeded(result);
+ onAuthenticationSucceeded();
+ Log.i(TAG, "SDP Unlock Succeeded, private key available for " +
+ "decryption through the AndroidKeyStore.\"");
+ try {
+ secureAuthCallback.authComplete(BiometricStatus.SUCCESS);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
+ super.onAuthenticationError(errorCode, errString);
+ Log.i(TAG, "SDP Unlock Error: " + errorCode + ": " + errString);
+ try {
+ onMessage(String.valueOf(errString));
+ onAuthenticationFailed();
+ secureAuthCallback.authComplete(BiometricStatus.FAILED);
+ } catch (SecurityException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ super.onAuthenticationFailed();
+ Log.i(TAG, "SDP Unlock Failed");
+ try {
+ onAuthenticationFailed();
+ secureAuthCallback.authComplete(BiometricStatus.FAILED);
+ } catch (SecurityException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ /**
+ * Show the auth dialog
+ *
+ * @param title The title of the prompt
+ * @param subtitle subtitle of the prompt
+ * @param description description of the prompt
+ * @param negativeButtonText cancel button text (
+ * @param callback The callback to handle auth complete, failures, etc
+ * @param cryptoObject The crypto object to authenticate (Cipher, Signature, etc)
+ */
+ public void showAuthDialog(String title, String subtitle,
+ String description,
+ String negativeButtonText,
+ SecureCipher.SecureAuthCallback callback,
+ BiometricPrompt.CryptoObject cryptoObject) {
+ this.secureAuthCallback = callback;
+
+ //activity.runOnUiThread(() -> {
+ BiometricPrompt.PromptInfo.Builder promptInfoBuilder =
+ new BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setSubtitle(subtitle)
+ .setDescription(description);
+ if (useDeviceCredential) {
+ promptInfoBuilder.setDeviceCredentialAllowed(true);
+ } else {
+ promptInfoBuilder.setNegativeButtonText(negativeButtonText);
+ }
+ BiometricPrompt.PromptInfo promptInfo = promptInfoBuilder.build();
+ try {
+ if (useDeviceCredential) {
+ Log.i(TAG, "Calling BiometricPrompt Authenticate for device credential.");
+ biometricPrompt.authenticate(promptInfo);
+ } else {
+ Log.i(TAG, "Calling BiometricPrompt Authenticate for biometrics.");
+ biometricPrompt.authenticate(promptInfo, cryptoObject);
+ }
+ } catch (IllegalArgumentException ex) {
+ Log.i(TAG, "Could not authenticate, make sure you have biometrics setup " +
+ "properly, if you set useDeviceCredential to true, you must not have " +
+ "biometrics available on the device");
+ }
+ //});
+ }
+
+ /**
+ * Authenticate a cipher using Biometrics to unlock the AndroidKeyStore key
+ *
+ * @param cipher The cipher to authenticate
+ * @param callback The callback to call when auth is complete with the appropriate event
+ */
+ public void authenticate(final Cipher cipher, SecureCipher.SecureAuthCallback callback) {
+ final BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(cipher);
+ showAuthDialog("Please Auth for key usage.",
+ "Key used for encrypting files",
+ "User authentication required to access key.",
+ "Cancel",
+ callback,
+ cryptoObject);
+ }
+
+ /**
+ * Authenticate a device using Device Credential to unlock the time-bound AndroidKeyStore keys
+ *
+ * The written file will be encrypted with the default keyPairAlias.
+ *
+ * @param name The name of the file to open; can not contain path separators.
+ * @param mode Operating mode.
+ * @return The resulting {@link FileOutputStream}.
+ * @throws IOException
+ */
+ public FileOutputStream openEncryptedFileOutput(@NonNull String name, @NonNull int mode,
+ boolean keyLocked)
+ throws IOException {
+ return openEncryptedFileOutput(name, mode, DEFAULT_FILE_ENCRYPTION_KEY, keyLocked);
+ }
+
+ /**
+ * Open a private encrypted file associated with this Context's application package for writing.
+ * Creates the file if it doesn't already exist.
+ *
+ * The written file will be encrypted with the specified keyPairAlias.
+ *
+ * @param name The name of the file to open; can not contain path separators.
+ * @param mode Operating mode.
+ * @param keyPairAlias The alias of the KeyPair used for encryption, the KeyPair will be
+ * created if it does not exist.
+ * @return The resulting {@link FileOutputStream}.
+ * @throws IOException
+ */
+ public FileOutputStream openEncryptedFileOutput(@NonNull String name,
+ @NonNull int mode,
+ @NonNull String keyPairAlias,
+ boolean keyLocked)
+ throws IOException {
+ if (keyLocked) {
+ FileCipher fileCipher = new FileCipher(keyPairAlias,
+ mContext.openFileOutput(name, mode),
+ mSecureConfig);
+ return fileCipher.getFileOutputStream();
+ } else {
+ AuthenticatedFileCipher authenticatedFileCipher = new AuthenticatedFileCipher(
+ keyPairAlias,
+ mContext.openFileOutput(name, mode),
+ mSecureConfig);
+ return authenticatedFileCipher.getFileOutputStream();
+ }
+ }
+
+
+ /**
+ * Checks if the device is locked
+ *
+ * @return true if the device is locked, false otherwise
+ */
+ public boolean deviceLocked() {
+ KeyguardManager keyGuardManager =
+ (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+ return keyGuardManager.isDeviceLocked();
+ }
+
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/AuthenticatedFileCipher.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/AuthenticatedFileCipher.java
new file mode 100644
index 0000000..90bb82e
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/AuthenticatedFileCipher.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec.crypto;
+
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.certifications.niap.niapsec.SecureConfig;
+import com.android.certifications.niap.niapsec.context.SecureContextCompat;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Combines Cipher and File to allow for easy to write and read encrypted files. This assumes that
+ * the AndroidKeyStore has been unlocked and keys are available for use.
+ *
+ * As per NIAP, this class encrypts the file contents with an ephemeral symmetric data encryption
+ * key and encodes the necessary information for decryption. The ephemeral key is encrypted with an
+ * asymmetric key encryption key.
+ */
+public class AuthenticatedFileCipher {
+
+ private String mFileName;
+ private String mKeyPairAlias;
+ private FileInputStream mFileInputStream;
+ private FileOutputStream mFileOutputStream;
+
+ private SecureContextCompat.EncryptedFileInputStreamListener mListener;
+
+ private SecureConfig mSecureConfig;
+
+ /**
+ * Instantiates a FileCipher to handle read. Encryption and decryption is
+ * handled internally.
+ *
+ * @param fileName The file path of the file to open
+ * @param fileInputStream The input stream of the File to read
+ * @param secureConfig The secure configuration used, which specifies algorithms, key sizes, etc
+ * @throws IOException When there is a file read issue
+ */
+ public AuthenticatedFileCipher(String fileName, FileInputStream fileInputStream,
+ SecureConfig secureConfig,
+ SecureContextCompat.EncryptedFileInputStreamListener listener)
+ throws IOException {
+ mFileName = fileName;
+ mFileInputStream = fileInputStream;
+ mSecureConfig = secureConfig;
+ EncryptedFileInputStream encryptedFileInputStream =
+ new EncryptedFileInputStream(mFileInputStream);
+ setEncryptedFileInputStreamListener(listener);
+ encryptedFileInputStream.decrypt(listener);
+ }
+
+ /**
+ * Instantiates a FileCipher to handle read. Encryption and decryption is
+ * handled internally.
+ *
+ * @param keyPairAlias The RSA key pair alias of the key stored in the AndroidKeyStore
+ * @param fileOutputStream The output stream for writing
+ * @param secureConfig The secure configuration used, which specifies algorithms,
+ * key sizes, etc
+ */
+ public AuthenticatedFileCipher(String keyPairAlias, FileOutputStream fileOutputStream,
+ SecureConfig secureConfig) {
+ mKeyPairAlias = keyPairAlias;
+ mFileOutputStream = new EncryptedFileOutputStream(
+ mFileName,
+ mKeyPairAlias,
+ fileOutputStream);
+ mSecureConfig = secureConfig;
+ }
+
+ /**
+ * Set the executor and listener for keystore backed authenticated requests
+ *
+ * @param listener The listener which is called after a biometric authorization
+ */
+ public void setEncryptedFileInputStreamListener(
+ SecureContextCompat.EncryptedFileInputStreamListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * @return The file output stream for writing
+ */
+ public FileOutputStream getFileOutputStream() {
+ return mFileOutputStream;
+ }
+
+ /**
+ * @return The input stream for reading
+ */
+ public FileInputStream getFileInputStream() {
+ return mFileInputStream;
+ }
+
+ /**
+ * Internal class adding encryption to writes
+ */
+ class EncryptedFileOutputStream extends FileOutputStream {
+ private static final String WRITE_NOT_SUPPORTED = "For encrypted files, you must write " +
+ "all data simultaneously. Call #write(byte[]).";
+
+ private static final String TAG = "EncryptedFOS";
+
+ private FileOutputStream fileOutputStream;
+ private String keyPairAlias;
+
+ EncryptedFileOutputStream(String name,
+ String keyPairAlias,
+ FileOutputStream fileOutputStream) {
+ super(new FileDescriptor());
+ this.keyPairAlias = keyPairAlias;
+ this.fileOutputStream = fileOutputStream;
+ }
+
+ private String getAsymKeyPairAlias() {
+ return this.keyPairAlias;
+ }
+
+ @Override
+ public void write(@NonNull byte[] b) {
+ SecureKeyStore secureKeyStore = SecureKeyStore.getDefault(mSecureConfig);
+ if (!secureKeyStore.keyExists(getAsymKeyPairAlias())) {
+ SecureKeyGenerator keyGenerator = SecureKeyGenerator.getInstance(mSecureConfig);
+ keyGenerator.generateAsymmetricKeyPair(getAsymKeyPairAlias());
+ }
+ SecureKeyGenerator secureKeyGenerator = SecureKeyGenerator.getInstance(mSecureConfig);
+ EphemeralSecretKey secretKey = secureKeyGenerator.generateEphemeralDataKey();
+ if (mSecureConfig.isDebugLoggingEnabled()) {
+ Log.i("AuthenticatedFileCipher", "Calling: " + SecureRandom.class.getSimpleName() +
+ " EphemeralSecretKey: Base64:\n" +
+ Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT));
+ }
+ SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig);
+ Pair encryptedData = secureCipher.encryptEphemeralData(secretKey, b,
+ keyPairAlias);
+ byte[] encryptedEphemeralKey = secureCipher.encryptSensitiveDataAsymmetric(
+ getAsymKeyPairAlias(),
+ secretKey.getEncoded());
+ secretKey.destroy();
+ byte[] encodedData = secureCipher.encodeEphemeralData(
+ getAsymKeyPairAlias().getBytes(),
+ encryptedEphemeralKey,
+ encryptedData.first,
+ encryptedData.second);
+ try {
+ fileOutputStream.write(encodedData);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write secure file.");
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public void write(@NonNull byte[] b, int off, int len) throws IOException {
+ throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public void close() throws IOException {
+ fileOutputStream.close();
+ }
+
+ @NonNull
+ @Override
+ public FileChannel getChannel() {
+ throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED);
+ }
+
+ @Override
+ protected void finalize() throws IOException {
+ super.finalize();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ fileOutputStream.flush();
+ }
+ }
+
+ /**
+ * Internal class adding encryption to reads
+ */
+ class EncryptedFileInputStream extends FileInputStream {
+ private static final String READ_NOT_SUPPORTED = "For encrypted files, you must read all " +
+ "data simultaneously. Call #read(byte[]).";
+
+ // Was 25 characters, truncating to fix compile error
+ private static final String TAG = "EncryptedFIS";
+
+ private FileInputStream fileInputStream;
+ private byte[] decryptedData;
+ private int readStatus = 0;
+
+ EncryptedFileInputStream(FileInputStream fileInputStream) {
+ super(new FileDescriptor());
+ this.fileInputStream = fileInputStream;
+ }
+
+ @Override
+ public int read() throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ void decrypt(SecureContextCompat.EncryptedFileInputStreamListener listener)
+ throws IOException {
+ if (this.decryptedData == null) {
+ try {
+ byte[] encodedData = new byte[fileInputStream.available()];
+ readStatus = fileInputStream.read(encodedData);
+ SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig);
+ this.decryptedData = secureCipher.decryptEncodedData(encodedData);
+ listener.onEncryptedFileInput(this);
+ } catch (IOException ex) {
+ throw ex;
+ }
+ }
+ }
+
+ private void destroyCache() {
+ if (decryptedData != null) {
+ Arrays.fill(decryptedData, (byte) 0);
+ decryptedData = null;
+ }
+ }
+
+ @Override
+ public int read(@NonNull byte[] b) {
+ System.arraycopy(decryptedData, 0, b, 0, decryptedData.length);
+ return readStatus;
+ }
+
+ // TODO, implement this
+ @Override
+ public int read(@NonNull byte[] b, int off, int len) throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ // TODO, implement this
+ @Override
+ public long skip(long n) throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ public int available() {
+ return decryptedData.length;
+ }
+
+ @Override
+ public void close() throws IOException {
+ destroyCache();
+ fileInputStream.close();
+ }
+
+ @Override
+ public FileChannel getChannel() {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ protected void finalize() throws IOException {
+ destroyCache();
+ super.finalize();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ }
+
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/EphemeralSecretKey.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/EphemeralSecretKey.java
new file mode 100644
index 0000000..7ae074e
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/EphemeralSecretKey.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec.crypto;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import com.android.certifications.niap.niapsec.SecureConfig;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.KeySpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.DESKeySpec;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.OAEPParameterSpec;
+import javax.crypto.spec.PSource;
+
+/**
+ * An implementation of SecretKey and KeySpec that has the ability to zero out the key material
+ * to make it harder to access the contents in a memory dump.
+ */
+public class EphemeralSecretKey implements KeySpec, SecretKey {
+
+ private static final long serialVersionUID = 7177238317307289223L;
+ private static final String TAG = "EphemeralSecretKey";
+
+ private byte[] key;
+
+ private String algorithm;
+
+ private boolean keyDestroyed;
+
+ private SecureConfig secureConfig;
+
+ /**
+ * Instantiate a secret key from an existing key
+ *
+ * @param key The a generated key created from {@link SecureKeyGenerator}
+ */
+ public EphemeralSecretKey(byte[] key, SecureConfig secureConfig) {
+ this(key, KeyProperties.KEY_ALGORITHM_AES, secureConfig);
+ }
+
+ /**
+ * Constructs a secret key from the given byte array.
+ *
+ *
This constructor does not check if the given bytes indeed specify a
+ * secret key of the specified algorithm. For example, if the algorithm is
+ * DES, this constructor does not check if key is 8 bytes
+ * long, and also does not check for weak or semi-weak keys.
+ * In order for those checks to be performed, an algorithm-specific
+ * key specification class (in this case:
+ * {@link DESKeySpec DESKeySpec})
+ * should be used.
+ *
+ * @param key the key material of the secret key. The contents of
+ * the array are copied to protect against subsequent modification.
+ * @param algorithm the name of the secret-key algorithm to be associated
+ * with the given key material.
+ * See Appendix A in the
+ * Java Cryptography Architecture Reference Guide
+ * for information about standard algorithm names.
+ * @throws IllegalArgumentException if algorithm
+ * is null or key is null or empty.
+ */
+ public EphemeralSecretKey(byte[] key, String algorithm, SecureConfig secureConfig) {
+ if (key == null || algorithm == null) {
+ throw new IllegalArgumentException("Missing argument");
+ }
+ if (key.length == 0) {
+ throw new IllegalArgumentException("Empty key");
+ }
+ this.key = key;
+ this.algorithm = algorithm;
+ this.keyDestroyed = false;
+ this.secureConfig = secureConfig;
+ }
+
+ /**
+ * Constructs a secret key from the given byte array, using the first
+ * len bytes of key, starting at
+ * offset inclusive.
+ *
+ *
The bytes that constitute the secret key are
+ * those between key[offset] and
+ * key[offset+len-1] inclusive.
+ *
+ *
This constructor does not check if the given bytes indeed specify a
+ * secret key of the specified algorithm. For example, if the algorithm is
+ * DES, this constructor does not check if key is 8 bytes
+ * long, and also does not check for weak or semi-weak keys.
+ * In order for those checks to be performed, an algorithm-specific key
+ * specification class (in this case:
+ * {@link DESKeySpec DESKeySpec})
+ * must be used.
+ *
+ * @param key the key material of the secret key. The first
+ * len bytes of the array beginning at
+ * offset inclusive are copied to protect
+ * against subsequent modification.
+ * @param offset the offset in key where the key material
+ * starts.
+ * @param len the length of the key material.
+ * @param algorithm the name of the secret-key algorithm to be associated
+ * with the given key material.
+ * See Appendix A in the
+ * Java Cryptography Architecture Reference Guide
+ * for information about standard algorithm names.
+ * @throws IllegalArgumentException if algorithm
+ * is null or key is null, empty, or too short,
+ * i.e. {@code key.length-offsetoffset or len index bytes outside the
+ * key.
+ */
+ public EphemeralSecretKey(byte[] key, int offset, int len, String algorithm) {
+ if (key == null || algorithm == null) {
+ throw new IllegalArgumentException("Missing argument");
+ }
+ if (key.length == 0) {
+ throw new IllegalArgumentException("Empty key");
+ }
+ if (key.length - offset < len) {
+ throw new IllegalArgumentException
+ ("Invalid offset/length combination");
+ }
+ if (len < 0) {
+ throw new ArrayIndexOutOfBoundsException("len is negative");
+ }
+ this.key = new byte[len];
+ System.arraycopy(key, offset, this.key, 0, len);
+ this.algorithm = algorithm;
+ this.keyDestroyed = false;
+ }
+
+ /**
+ * Returns the name of the algorithm associated with this secret key.
+ *
+ * @return the secret key algorithm.
+ */
+ public String getAlgorithm() {
+ return this.algorithm;
+ }
+
+ /**
+ * Returns the name of the encoding format for this secret key.
+ *
+ * @return the string "RAW".
+ */
+ public String getFormat() {
+ return "RAW";
+ }
+
+ /**
+ * Returns the key material of this secret key.
+ *
+ * @return the key material. Returns a new array
+ * each time this method is called.
+ */
+ public byte[] getEncoded() {
+ return this.key;
+ }
+
+ /**
+ * Calculates a hash code value for the object.
+ * Objects that are equal will also have the same hashcode.
+ */
+ public int hashCode() {
+ int retval = 0;
+ for (int i = 1; i < this.key.length; i++) {
+ retval += this.key[i] * i;
+ }
+ if (this.algorithm.equalsIgnoreCase("TripleDES"))
+ return (retval ^= "desede".hashCode());
+ else
+ return (retval ^=
+ this.algorithm.toLowerCase(Locale.ENGLISH).hashCode());
+ }
+
+ /**
+ * Tests for equality between the specified object and this
+ * object. Two SecretKeySpec objects are considered equal if
+ * they are both SecretKey instances which have the
+ * same case-insensitive algorithm name and key encoding.
+ *
+ * @param obj the object to test for equality with this object.
+ * @return true if the objects are considered equal, false if
+ * obj is null or otherwise.
+ */
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+
+ if (!(obj instanceof SecretKey))
+ return false;
+
+ String thatAlg = ((SecretKey) obj).getAlgorithm();
+ if (!(thatAlg.equalsIgnoreCase(this.algorithm))) {
+ if ((!(thatAlg.equalsIgnoreCase("DESede"))
+ || !(this.algorithm.equalsIgnoreCase("TripleDES")))
+ && (!(thatAlg.equalsIgnoreCase("TripleDES"))
+ || !(this.algorithm.equalsIgnoreCase("DESede"))))
+ return false;
+ }
+
+ byte[] thatKey = ((SecretKey) obj).getEncoded();
+
+ return MessageDigest.isEqual(this.key, thatKey);
+ }
+
+ /**
+ * Overriding destroy to actually destroy the key, the default SecretKey implementations don't
+ * actually do antying in destroy to remove the key from memory.
+ */
+ @Override
+ public void destroy() {
+ if (!keyDestroyed) {
+ Arrays.fill(key, (byte) 0);
+ this.key = null;
+ keyDestroyed = true;
+ System.gc();
+ }
+ }
+
+
+ /**
+ * Clears the current key stored in this object.
+ *
+ * @param cipher The cipher used when operating with the key
+ * @param opmode The opmode used for Cipher creation
+ */
+ public void destroyCipherKey(Cipher cipher, int opmode, String keyPairAlias) {
+ try {
+ byte[] blankKey = new byte[secureConfig.getSymmetricKeySize() / 8];
+ byte[] iv = new byte[SecureConfig.AES_IV_SIZE_BYTES];
+ Arrays.fill(blankKey, (byte) 0);
+ Arrays.fill(iv, (byte) 0);
+ EphemeralSecretKey blankSecretKey = new EphemeralSecretKey(
+ blankKey,
+ secureConfig.getSymmetricKeyAlgorithm(),
+ secureConfig);
+ cipher.init(opmode, blankSecretKey, new GCMParameterSpec(
+ secureConfig.getSymmetricGcmTagLength(),
+ iv));
+ teeGC(keyPairAlias);
+ System.gc();
+ } catch (GeneralSecurityException e) {
+ throw new SecurityException("Could not destroy key.");
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return keyDestroyed;
+ }
+
+ private void teeGC(String keyPairAlias) {
+ try {
+ int iterations = secureConfig.getTeeGCIterations();
+ List arbitraryEncryptedAESKeys = new ArrayList<>();
+ SecureRandom secureRandom = SecureRandom.getInstanceStrong();
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ PublicKey publicKey = keyStore.getCertificate(keyPairAlias).getPublicKey();
+ Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
+ for (int i = 0; i < iterations; i++) {
+ byte[] key = new byte[256 / 8];
+ secureRandom.nextBytes(key);
+ cipher.init(Cipher.ENCRYPT_MODE,
+ publicKey,
+ new OAEPParameterSpec("SHA-256",
+ "MGF1",
+ new MGF1ParameterSpec("SHA-1"),
+ PSource.PSpecified.DEFAULT));
+ byte[] bytes = cipher.doFinal(key);
+ if (bytes != null) {
+ arbitraryEncryptedAESKeys.add(bytes);
+ }
+ }
+ PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyPairAlias, null);
+ for (int i = 0; i < iterations; i++) {
+ byte[] aesKey = arbitraryEncryptedAESKeys.get(i);
+ cipher.init(Cipher.DECRYPT_MODE,
+ privateKey,
+ new OAEPParameterSpec(
+ "SHA-256",
+ "MGF1",
+ new MGF1ParameterSpec("SHA-1"),
+ PSource.PSpecified.DEFAULT));
+ byte[] rawKey = cipher.doFinal(aesKey);
+ }
+ arbitraryEncryptedAESKeys.clear();
+ } catch (Exception ex) {
+
+ }
+ System.gc();
+ System.runFinalization();
+ }
+
+}
+
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/FileCipher.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/FileCipher.java
new file mode 100644
index 0000000..89f090a
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/FileCipher.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec.crypto;
+
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+
+import androidx.annotation.NonNull;
+import com.android.certifications.niap.niapsec.SecureConfig;
+import com.android.certifications.niap.niapsec.context.SecureContextCompat;
+
+/**
+ * Combines Cipher and File to allow for easy to write and read encrypted files.
+ *
+ * As per NIAP, this class encrypts the file contents with an ephemeral symmetric data encryption
+ * key and encodes the necessary information for decryption. The ephemeral key is encrypted with an
+ * asymmetric key encryption key.
+ */
+public class FileCipher {
+
+ private String mFileName;
+ private String mKeyPairAlias;
+ private FileInputStream mFileInputStream;
+ private FileOutputStream mFileOutputStream;
+
+ private Executor mExecutor;
+ private SecureContextCompat.EncryptedFileInputStreamListener mListener;
+
+ private SecureConfig mSecureConfig;
+
+ /**
+ * Instantiates a FileCipher to handle read. Encryption and decryption is
+ * handled internally.
+ *
+ * @param fileName The file path of the file to open
+ * @param fileInputStream The input stream of the File to read
+ * @param secureConfig The secure configuration used, which specifies algorithms, key sizes, etc
+ * @param executor An executor
+ * @param listener The listener for callbacks, this is necessary for key auth using
+ * BiometricPrompt
+ * @throws IOException When there is a file read issue
+ */
+ public FileCipher(String fileName, FileInputStream fileInputStream,
+ SecureConfig secureConfig, Executor executor,
+ SecureContextCompat.EncryptedFileInputStreamListener listener)
+ throws IOException {
+ mFileName = fileName;
+ mFileInputStream = fileInputStream;
+ mSecureConfig = secureConfig;
+ EncryptedFileInputStream encryptedFileInputStream =
+ new EncryptedFileInputStream(mFileInputStream);
+ setEncryptedFileInputStreamListener(executor, listener);
+ encryptedFileInputStream.decrypt(listener);
+ }
+
+ /**
+ * Instantiates a FileCipher to handle read. Encryption and decryption is
+ * handled internally.
+ *
+ * @param keyPairAlias The RSA key pair alias of the key stored in the AndroidKeyStore
+ * @param fileOutputStream The output stream for writing
+ * @param secureConfig The secure configuration used, which specifies algorithms, key sizes, etc
+ */
+ public FileCipher(String keyPairAlias, FileOutputStream fileOutputStream,
+ SecureConfig secureConfig) {
+ mKeyPairAlias = keyPairAlias;
+ mFileOutputStream = new EncryptedFileOutputStream(
+ mFileName,
+ mKeyPairAlias,
+ fileOutputStream);
+ mSecureConfig = secureConfig;
+ }
+
+ /**
+ * Set the executor and listener for keystore backed authenticated requests
+ *
+ * @param executor The executor for the callback
+ * @param listener The listener which is called after a biometric authorization
+ */
+ public void setEncryptedFileInputStreamListener(@NonNull Executor executor,
+ @NonNull
+ SecureContextCompat
+ .EncryptedFileInputStreamListener
+ listener) {
+ mExecutor = executor;
+ mListener = listener;
+ }
+
+ /**
+ * @return The file output stream for writing
+ */
+ public FileOutputStream getFileOutputStream() {
+ return mFileOutputStream;
+ }
+
+ /**
+ * @return The input stream for reading
+ */
+ public FileInputStream getFileInputStream() {
+ return mFileInputStream;
+ }
+
+ /**
+ * Internal class adding encryption to writes
+ */
+ class EncryptedFileOutputStream extends FileOutputStream {
+ private static final String WRITE_NOT_SUPPORTED = "For encrypted files, you must write " +
+ "all data simultaneously. Call #write(byte[]).";
+
+ private static final String TAG = "EncryptedFOS";
+
+ private FileOutputStream fileOutputStream;
+ private String keyPairAlias;
+
+ EncryptedFileOutputStream(String name,
+ String keyPairAlias,
+ FileOutputStream fileOutputStream) {
+ super(new FileDescriptor());
+ this.keyPairAlias = keyPairAlias;
+ this.fileOutputStream = fileOutputStream;
+ }
+
+ private String getAsymKeyPairAlias() {
+ return this.keyPairAlias;
+ }
+
+ @Override
+ public void write(@NonNull byte[] b) {
+ SecureKeyStore secureKeyStore = SecureKeyStore.getDefault(mSecureConfig);
+ if (!secureKeyStore.keyExists(getAsymKeyPairAlias())) {
+ SecureKeyGenerator keyGenerator = SecureKeyGenerator.getInstance(mSecureConfig);
+ keyGenerator.generateAsymmetricKeyPair(getAsymKeyPairAlias());
+ }
+ SecureKeyGenerator secureKeyGenerator = SecureKeyGenerator.getInstance(mSecureConfig);
+ EphemeralSecretKey secretKey = secureKeyGenerator.generateEphemeralDataKey();
+ if(mSecureConfig.isDebugLoggingEnabled()) {
+ Log.i("FileCipher", "Calling: " + SecureRandom.class.getSimpleName() +
+ " EphemeralSecretKey: Base64:\n" +
+ Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT));
+ }
+ SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig);
+ Pair encryptedData = secureCipher.encryptEphemeralData(secretKey, b,
+ keyPairAlias);
+ secureCipher.encryptSensitiveDataAsymmetric(
+ getAsymKeyPairAlias(),
+ secretKey.getEncoded(),
+ (byte[] encryptedEphemeralKey) -> {
+ byte[] encodedData = secureCipher.encodeEphemeralData(
+ getAsymKeyPairAlias().getBytes(),
+ encryptedEphemeralKey,
+ encryptedData.first,
+ encryptedData.second);
+ secretKey.destroy();
+ try {
+ fileOutputStream.write(encodedData);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write secure file.");
+ e.printStackTrace();
+ }
+ });
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public void write(@NonNull byte[] b, int off, int len) throws IOException {
+ throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED);
+ }
+
+ @Override
+ public void close() throws IOException {
+ fileOutputStream.close();
+ }
+
+ @NonNull
+ @Override
+ public FileChannel getChannel() {
+ throw new UnsupportedOperationException(WRITE_NOT_SUPPORTED);
+ }
+
+ @Override
+ protected void finalize() throws IOException {
+ super.finalize();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ fileOutputStream.flush();
+ }
+ }
+
+ /**
+ * Internal class adding encryption to reads
+ */
+ class EncryptedFileInputStream extends FileInputStream {
+ private static final String READ_NOT_SUPPORTED = "For encrypted files, you must read all " +
+ "data simultaneously. Call #read(byte[]).";
+
+ // Was 25 characters, truncating to fix compile error
+ private static final String TAG = "EncryptedFIS";
+
+ private FileInputStream fileInputStream;
+ private byte[] decryptedData;
+ private int readStatus = 0;
+
+ EncryptedFileInputStream(FileInputStream fileInputStream) {
+ super(new FileDescriptor());
+ this.fileInputStream = fileInputStream;
+ }
+
+ @Override
+ public int read() throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ void decrypt(SecureContextCompat.EncryptedFileInputStreamListener listener)
+ throws IOException {
+ if (this.decryptedData == null) {
+ try {
+ byte[] encodedData = new byte[fileInputStream.available()];
+ readStatus = fileInputStream.read(encodedData);
+ SecureCipher secureCipher = SecureCipher.getDefault(mSecureConfig);
+ secureCipher.decryptEncodedData(encodedData, decryptedData -> {
+ this.decryptedData = decryptedData;
+ //Binder.clearCallingIdentity();
+ listener.onEncryptedFileInput(this);
+ });
+ } catch (IOException ex) {
+ throw ex;
+ }
+ }
+ }
+
+ private void destroyCache() {
+ if (decryptedData != null) {
+ Arrays.fill(decryptedData, (byte) 0);
+ decryptedData = null;
+ }
+ }
+
+ @Override
+ public int read(@NonNull byte[] b) {
+ System.arraycopy(decryptedData, 0, b, 0, decryptedData.length);
+ return readStatus;
+ }
+
+ // TODO, implement this
+ @Override
+ public int read(@NonNull byte[] b, int off, int len) throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ // TODO, implement this
+ @Override
+ public long skip(long n) throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ public int available() {
+ return decryptedData.length;
+ }
+
+ @Override
+ public void close() throws IOException {
+ destroyCache();
+ fileInputStream.close();
+ }
+
+ @Override
+ public FileChannel getChannel() {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ protected void finalize() throws IOException {
+ destroyCache();
+ super.finalize();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ throw new UnsupportedOperationException(READ_NOT_SUPPORTED);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ }
+
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureCipher.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureCipher.java
new file mode 100644
index 0000000..a2a20b8
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureCipher.java
@@ -0,0 +1,760 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec.crypto;
+
+
+import android.os.Build;
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+
+import com.android.certifications.niap.niapsec.SecureConfig;
+import com.android.certifications.niap.niapsec.biometric.BiometricSupport;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.MGF1ParameterSpec;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.OAEPParameterSpec;
+import javax.crypto.spec.PSource;
+
+/**
+ * Wraps {@link Cipher} to provide NIAP Sensitive data protection.
+ *
+ * Adds encryption options to encrypt data using both a data and key encryption key.
+ */
+public class SecureCipher {
+
+ private static final String TAG = "SecureCipher";
+
+ private SecureConfig secureConfig;
+
+ public static int MODE_ENCRYPT = 1;
+ public static int MODE_DECRYPT = 2;
+
+ /**
+ * Generic type used to handle callbacks from the SecureCipher
+ */
+ public interface SecureCallback {
+ }
+
+ /**
+ * Callback interface for specifying authentication
+ */
+ public interface SecureAuthCallback extends SecureCallback {
+ void authComplete(BiometricSupport.BiometricStatus status);
+ }
+
+ /**
+ * Callback interface for specifying asynchronous and authenticated symmetric encryption
+ */
+ public interface SecureSymmetricEncryptionCallback extends SecureCallback {
+ void encryptionComplete(byte[] cipherText, byte[] iv);
+ }
+
+ /**
+ * Callback interface for specifying asynchronous and authenticated asymmetric encryption
+ */
+ public interface SecureAsymmetricEncryptionCallback extends SecureCallback {
+ void encryptionComplete(byte[] cipherText);
+ }
+
+ /**
+ * Callback interface for specifying asynchronous and authenticated decryption
+ */
+ public interface SecureDecryptionCallback extends SecureCallback {
+ void decryptionComplete(byte[] clearText);
+ }
+
+ /**
+ * Create and return a SecureCipher with NIAP recommended settings that requires biometric
+ * authentication for key use
+ *
+ * @param secureConfig The config to use
+ * @return A secure cipher with NIAP recommended settings
+ */
+ public static SecureCipher getDefault(SecureConfig secureConfig) {
+ return new SecureCipher(secureConfig);
+ }
+
+ /**
+ * Instantiates a SecureConfig with the provided config
+ *
+ * @param secureConfig The settings to build the cipher with
+ */
+ private SecureCipher(SecureConfig secureConfig) {
+ this.secureConfig = secureConfig;
+ }
+
+ /**
+ * Encoding types used internally
+ */
+ enum SecureFileEncodingType {
+ SYMMETRIC(0),
+ ASYMMETRIC(1),
+ EPHEMERAL(2),
+ NOT_ENCRYPTED(1000);
+
+ private final int type;
+
+ SecureFileEncodingType(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public static SecureFileEncodingType fromId(int id) {
+ switch (id) {
+ case 0:
+ return SYMMETRIC;
+ case 1:
+ return ASYMMETRIC;
+ case 2:
+ return EPHEMERAL;
+ }
+ return NOT_ENCRYPTED;
+ }
+
+ }
+
+ /**
+ * Encrypts data with an existing key alias from the AndroidKeyStore.
+ *
+ * @param keyAlias The name of the existing SecretKey to retrieve from the AndroidKeyStore.
+ * @param clearData The unencrypted data to encrypt
+ * @return A Pair of byte[]'s, first is the encrypted data, second is the IV
+ * (initialization vector) used to encrypt which is required for decryption
+ */
+ public void encryptSensitiveData(String keyAlias,
+ byte[] clearData,
+ SecureSymmetricEncryptionCallback callback) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ SecretKey key = (SecretKey) keyStore.getKey(keyAlias, null);
+ Cipher cipher = Cipher.getInstance(secureConfig.getSymmetricCipherTransformation());
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] iv = cipher.getIV();
+ if (secureConfig.isSymmetricRequireUserAuthEnabled()) {
+ secureConfig.getBiometricSupport().authenticate(
+ cipher,
+ (BiometricSupport.BiometricStatus status) -> {
+ switch (status) {
+ case SUCCESS:
+ try {
+ callback.encryptionComplete(
+ cipher.doFinal(clearData),
+ cipher.getIV());
+ } catch (GeneralSecurityException e) {
+ e.printStackTrace();
+ }
+ break;
+ default:
+ Log.i(TAG, "Failure");
+ callback.encryptionComplete(null, null);
+ }
+ });
+ } else {
+ callback.encryptionComplete(cipher.doFinal(clearData), cipher.getIV());
+ }
+ } catch (GeneralSecurityException | IOException ex) {
+ Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ }
+ }
+
+ /**
+ * Encrypts data with a public key from the cert in the AndroidKeyStore.
+ *
+ * @param keyAlias The name of the existing KeyPair to retrieve the PublicKey from the
+ * AndroidKeyStore.
+ * @param clearData The unencrypted data to encrypt
+ * @return A Pair of byte[]'s, first is the encrypted data, second is the IV
+ * (initialization vector) used to encrypt which is required for decryption
+ */
+ public void encryptSensitiveDataAsymmetric(String keyAlias,
+ byte[] clearData,
+ SecureAsymmetricEncryptionCallback callback) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
+ Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation());
+ if (secureConfig.getAsymmetricPaddings().equals(
+ KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) {
+ cipher.init(Cipher.ENCRYPT_MODE,
+ publicKey,
+ new OAEPParameterSpec("SHA-256",
+ "MGF1",
+ new MGF1ParameterSpec("SHA-1"),
+ PSource.PSpecified.DEFAULT));
+ } else {
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+ }
+ byte[] clearText = cipher.doFinal(clearData);
+ callback.encryptionComplete(clearText);
+ } catch (GeneralSecurityException | IOException ex) {
+ Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ }
+ }
+
+ /**
+ * Encrypts data with a public key from the cert in the AndroidKeyStore.
+ *
+ * @param keyAlias The name of the existing KeyPair to retrieve the PublicKey from the
+ * AndroidKeyStore.
+ * @param clearData The unencrypted data to encrypt
+ * @return A Pair of byte[]'s, first is the encrypted data, second is the IV
+ * (initialization vector) used to encrypt which is required for decryption
+ */
+ public byte[] encryptSensitiveDataAsymmetric(String keyAlias,
+ byte[] clearData) {
+ byte[] cipherText = null;
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
+ Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation());
+ if (secureConfig.getAsymmetricPaddings().equals(
+ KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) {
+ cipher.init(Cipher.ENCRYPT_MODE,
+ publicKey,
+ new OAEPParameterSpec("SHA-256",
+ "MGF1",
+ new MGF1ParameterSpec("SHA-1"),
+ PSource.PSpecified.DEFAULT));
+ } else {
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+ }
+ cipherText = cipher.doFinal(clearData);
+ } catch (GeneralSecurityException | IOException ex) {
+ Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ }
+ return cipherText;
+ }
+
+ /**
+ * Encrypts data using an Ephemeral key, destroying any trace of the key from the Cipher used.
+ *
+ * @param ephemeralSecretKey The generated Ephemeral key
+ * @param clearData The unencrypted data to encrypt
+ * @return A Pair of byte[]'s, first is the encrypted data, second is the IV
+ * (initialization vector)
+ * used to encrypt which is required for decryption
+ */
+ public Pair encryptEphemeralData(EphemeralSecretKey ephemeralSecretKey,
+ byte[] clearData, String keyPairAlias) {
+ try {
+ SecureRandom secureRandom = null;
+ secureRandom = SecureRandom.getInstanceStrong();
+ /*
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+
+ } else {
+ secureRandom = new SecureRandom();
+ }
+ */
+ byte[] iv = new byte[SecureConfig.AES_IV_SIZE_BYTES];
+ secureRandom.nextBytes(iv);
+ GCMParameterSpec parameterSpec =
+ new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(), iv);
+ final Cipher cipher =
+ Cipher.getInstance(secureConfig.getSymmetricCipherTransformation());
+ cipher.init(Cipher.ENCRYPT_MODE, ephemeralSecretKey, parameterSpec);
+ byte[] encryptedData = cipher.doFinal(clearData);
+ ephemeralSecretKey.destroyCipherKey(cipher, Cipher.ENCRYPT_MODE, keyPairAlias);
+ return new Pair<>(encryptedData, iv);
+ } catch (GeneralSecurityException ex) {
+ Log.e(TAG, "Failure to encrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ }
+ }
+
+ /**
+ * Decrypts a previously encrypted byte[]
+ *
+ * Destroys all traces of the key data in the Cipher.
+ *
+ * @param ephemeralSecretKey The generated Ephemeral key
+ * @param encryptedData The byte[] of encrypted data
+ * @param initializationVector The IV of which the encrypted data was encrypted with
+ * @return The byte[] of data that has been decrypted
+ */
+ public byte[] decryptEphemeralData(EphemeralSecretKey ephemeralSecretKey,
+ byte[] encryptedData, byte[] initializationVector,
+ String keyPairAlias) {
+ try {
+ final Cipher cipher =
+ Cipher.getInstance(secureConfig.getSymmetricCipherTransformation());
+ cipher.init(Cipher.DECRYPT_MODE,
+ ephemeralSecretKey,
+ new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(),
+ initializationVector));
+ byte[] decryptedData = cipher.doFinal(encryptedData);
+ ephemeralSecretKey.destroyCipherKey(cipher, Cipher.DECRYPT_MODE, keyPairAlias);
+ return decryptedData;
+ } catch (GeneralSecurityException ex) {
+ Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ }
+ }
+
+ /**
+ * Decrypts a previously encrypted byte[]
+ *
+ * @param keyAlias The name of the existing SecretKey to retrieve from the
+ * AndroidKeyStore.
+ * @param encryptedData The byte[] of encrypted data
+ * @param initializationVector The IV of which the encrypted data was encrypted with
+ * @return The byte[] of data that has been decrypted
+ */
+ public byte[] decryptSensitiveData(String keyAlias, byte[] encryptedData,
+ byte[] initializationVector) {
+ byte[] decryptedData = new byte[0];
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ Key key = keyStore.getKey(keyAlias, null);
+ Cipher cipher = Cipher.getInstance(secureConfig.getSymmetricCipherTransformation());
+ GCMParameterSpec spec = new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(),
+ initializationVector);
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ decryptedData = cipher.doFinal(encryptedData);
+ } catch (GeneralSecurityException | IOException ex) {
+ Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ }
+ return decryptedData;
+ }
+
+ /**
+ * Decrypts a previously encrypted byte[]
+ *
+ * @param keyAlias The name of the existing SecretKey to retrieve from the
+ * AndroidKeyStore.
+ * @param encryptedData The byte[] of encrypted data
+ * @param initializationVector The IV of which the encrypted data was encrypted with
+ * @return The byte[] of data that has been decrypted
+ */
+ public void decryptSensitiveData(String keyAlias, byte[] encryptedData,
+ byte[] initializationVector,
+ SecureDecryptionCallback callback) {
+ byte[] decryptedData = new byte[0];
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ Key key = keyStore.getKey(keyAlias, null);
+ Cipher cipher = Cipher.getInstance(secureConfig.getSymmetricCipherTransformation());
+ GCMParameterSpec spec = new GCMParameterSpec(secureConfig.getSymmetricGcmTagLength(),
+ initializationVector);
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ if (secureConfig.isSymmetricRequireUserAuthEnabled()) {
+ secureConfig.getBiometricSupport().authenticate(
+ cipher,
+ (BiometricSupport.BiometricStatus status) -> {
+ switch (status) {
+ case SUCCESS:
+ try {
+ callback.decryptionComplete(cipher.doFinal(encryptedData));
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Failure to decrypt data: " +
+ e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ break;
+ default:
+ Log.e(TAG, "Failure to decrypt data: " +
+ "Biometric authentication failed");
+ callback.decryptionComplete(null);
+ }
+ });
+ } else {
+ callback.decryptionComplete(cipher.doFinal(encryptedData));
+ }
+ } catch (GeneralSecurityException ex) {
+ Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage());
+ throw new SecurityException(ex);
+ }
+ }
+
+ /**
+ * Decrypts a previously encrypted byte[] with the PrivateKey
+ *
+ * @param keyAlias The name of the existing KeyPair to retrieve from the AndroidKeyStore.
+ * @param encryptedData The byte[] of encrypted data
+ * @return The byte[] of data that has been decrypted
+ */
+ public byte[] decryptSensitiveDataAsymmetric(String keyAlias,
+ byte[] encryptedData) {
+ byte[] clearData = new byte[0];
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, null);
+ Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation());
+ if (secureConfig.getAsymmetricPaddings().equals(
+ KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) {
+ cipher.init(Cipher.DECRYPT_MODE,
+ key,
+ new OAEPParameterSpec(
+ "SHA-256",
+ "MGF1",
+ new MGF1ParameterSpec("SHA-1"),
+ PSource.PSpecified.DEFAULT));
+ } else {
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ }
+ clearData = cipher.doFinal(encryptedData);
+ } catch (GeneralSecurityException | IOException ex) {
+ Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage());
+ ex.printStackTrace();
+ }
+ return clearData;
+ }
+
+ /**
+ * Decrypts a previously encrypted byte[] with the PrivateKey
+ *
+ * @param keyAlias The name of the existing KeyPair to retrieve from the AndroidKeyStore.
+ * @param encryptedData The byte[] of encrypted data
+ * @return The byte[] of data that has been decrypted
+ */
+ public void decryptSensitiveDataAsymmetric(String keyAlias,
+ byte[] encryptedData,
+ SecureDecryptionCallback callback) {
+ byte[] decryptedData = new byte[0];
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, null);
+ Cipher cipher = Cipher.getInstance(secureConfig.getAsymmetricCipherTransformation());
+ // If the key was created using the time-bound method, we must delay creation of the
+ // Cipher object
+ if (secureConfig.getAsymmetricRequireUserValiditySeconds() <= 0) {
+ if (secureConfig.getAsymmetricPaddings().equals(
+ KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) {
+ cipher.init(Cipher.DECRYPT_MODE,
+ key,
+ new OAEPParameterSpec(
+ "SHA-256",
+ "MGF1",
+ new MGF1ParameterSpec("SHA-1"),
+ PSource.PSpecified.DEFAULT));
+ } else {
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ }
+ }
+ if (secureConfig.isAsymmetricRequireUserAuthEnabled()) {
+ if (secureConfig.getAsymmetricRequireUserValiditySeconds() <= 0) {
+ secureConfig.getBiometricSupport().authenticate(
+ cipher,
+ (BiometricSupport.BiometricStatus status) -> {
+ switch (status) {
+ case SUCCESS:
+ try {
+ byte[] clearData = cipher.doFinal(encryptedData);
+ callback.decryptionComplete(clearData);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Failure to decrypt data: " +
+ e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ break;
+ default:
+ Log.e(TAG, "Failure to decrypt data: " +
+ "Biometric authentication failed");
+ callback.decryptionComplete(null);
+ throw new SecurityException("Failure to decrypt data: " +
+ "Biometric authentication failed");
+ }
+ });
+ } else {
+ secureConfig.getBiometricSupport().authenticate(
+ cipher,
+ (BiometricSupport.BiometricStatus status) -> {
+ switch (status) {
+ case SUCCESS:
+ try {
+ if (secureConfig.getAsymmetricPaddings().equals(
+ KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) {
+ cipher.init(Cipher.DECRYPT_MODE,
+ key,
+ new OAEPParameterSpec(
+ "SHA-256",
+ "MGF1",
+ new MGF1ParameterSpec("SHA-1"),
+ PSource.PSpecified.DEFAULT));
+ } else {
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ }
+ byte[] clearData = cipher.doFinal(encryptedData);
+ callback.decryptionComplete(clearData);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Failure to decrypt data: " +
+ e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ break;
+ default:
+ Log.e(TAG, "Failure to decrypt data: " +
+ "Biometric authentication failed");
+ callback.decryptionComplete(null);
+ throw new SecurityException("Failure to decrypt data: " +
+ "Biometric authentication failed");
+ }
+ });
+
+ }
+
+
+ } else {
+ decryptedData = cipher.doFinal(encryptedData);
+ callback.decryptionComplete(decryptedData);
+ }
+ } catch (GeneralSecurityException | IOException ex) {
+ Log.e(TAG, "Failure to decrypt data: " + ex.getLocalizedMessage());
+ ex.printStackTrace();
+ }
+ }
+
+ /**
+ * Encode ephemeral data
+ *
+ * @param keyPairAlias The key pair alias of the RSA AndroidKeyStore used in encryption
+ * @param encryptedKey The encrypted key
+ * @param cipherText The encrypted data
+ * @param iv The IV created when the data was encrypted
+ * @return The encoded bytes
+ */
+ public byte[] encodeEphemeralData(byte[] keyPairAlias, byte[] encryptedKey,
+ byte[] cipherText, byte[] iv) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 4) + iv.length +
+ keyPairAlias.length + encryptedKey.length + cipherText.length);
+ byteBuffer.putInt(SecureFileEncodingType.EPHEMERAL.getType());
+ byteBuffer.putInt(encryptedKey.length);
+ byteBuffer.put(encryptedKey);
+ byteBuffer.putInt(iv.length);
+ byteBuffer.put(iv);
+ byteBuffer.putInt(keyPairAlias.length);
+ byteBuffer.put(keyPairAlias);
+ byteBuffer.put(cipherText);
+ return byteBuffer.array();
+ }
+
+ /**
+ * Encodes encrypted data
+ *
+ * @param keyAlias The key pair alias of the RSA AndroidKeyStore key that the
+ * data encryption key was encrypted with
+ * @param cipherText The encrypted data
+ * @param iv The IV created during the initialal encrypt operation
+ * @return The encoded data
+ */
+ public byte[] encodeSymmetricData(byte[] keyAlias, byte[] cipherText, byte[] iv) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 3) + iv.length +
+ keyAlias.length + cipherText.length);
+ byteBuffer.putInt(SecureFileEncodingType.SYMMETRIC.getType());
+ byteBuffer.putInt(iv.length);
+ byteBuffer.put(iv);
+ byteBuffer.putInt(keyAlias.length);
+ byteBuffer.put(keyAlias);
+ byteBuffer.put(cipherText);
+ return byteBuffer.array();
+ }
+
+ /**
+ * Encodes asymmetric data
+ *
+ * @param keyPairAlias The key pair alias of the RSA AndroidKeyStore key
+ * @param cipherText The cipher text that has been encrypted
+ * @return The encoded bytes
+ */
+ public byte[] encodeAsymmetricData(byte[] keyPairAlias, byte[] cipherText) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 2) +
+ keyPairAlias.length + cipherText.length);
+ byteBuffer.putInt(SecureFileEncodingType.ASYMMETRIC.getType());
+ byteBuffer.putInt(keyPairAlias.length);
+ byteBuffer.put(keyPairAlias);
+ byteBuffer.put(cipherText);
+ return byteBuffer.array();
+ }
+
+ /**
+ * Decrypts previous encrypted/encoded {@link SecureCipher}
+ *
+ * @param encodedCipherText The encoded cipher text
+ * @return decrypted data
+ */
+ public byte[] decryptEncodedData(byte[] encodedCipherText) {
+ byte[] clearText = new byte[0];
+ ByteBuffer byteBuffer = ByteBuffer.wrap(encodedCipherText);
+ int encodingTypeVal = byteBuffer.getInt();
+ SecureFileEncodingType encodingType = SecureFileEncodingType.fromId(encodingTypeVal);
+ byte[] encodedEphKey = null;
+ byte[] iv = null;
+ String keyAlias = null;
+ byte[] cipherText = null;
+
+ switch (encodingType) {
+ case EPHEMERAL:
+ int encodedEphKeyLength = byteBuffer.getInt();
+ encodedEphKey = new byte[encodedEphKeyLength];
+ byteBuffer.get(encodedEphKey);
+ case SYMMETRIC:
+ int ivLength = byteBuffer.getInt();
+ iv = new byte[ivLength];
+ byteBuffer.get(iv);
+ case ASYMMETRIC:
+ int keyAliasLength = byteBuffer.getInt();
+ byte[] keyAliasBytes = new byte[keyAliasLength];
+ byteBuffer.get(keyAliasBytes);
+ keyAlias = new String(keyAliasBytes);
+ cipherText = new byte[byteBuffer.remaining()];
+ byteBuffer.get(cipherText);
+ break;
+ case NOT_ENCRYPTED:
+ throw new SecurityException("Cannot determine file type.");
+ }
+ switch (encodingType) {
+ case EPHEMERAL:
+ final byte[] ephemeralCipherText = cipherText;
+ final byte[] ephemeralIv = iv;
+ String finalKeyAlias = keyAlias;
+ byte[] decryptedEphKey = decryptSensitiveDataAsymmetric(keyAlias,
+ encodedEphKey);
+ if (decryptedEphKey != null) {
+ EphemeralSecretKey ephemeralSecretKey =
+ new EphemeralSecretKey(decryptedEphKey, secureConfig);
+ clearText = decryptEphemeralData(
+ ephemeralSecretKey,
+ ephemeralCipherText, ephemeralIv, finalKeyAlias);
+ ephemeralSecretKey.destroy();
+ } else {
+ Log.i(TAG, "Key was null, this usually means there was an auth " +
+ "failure.");
+ }
+ break;
+ case SYMMETRIC:
+ clearText = decryptSensitiveData(
+ keyAlias,
+ cipherText, iv);
+ break;
+ case ASYMMETRIC:
+ clearText = decryptSensitiveDataAsymmetric(
+ keyAlias,
+ cipherText);
+ break;
+ case NOT_ENCRYPTED:
+ throw new SecurityException("File not encrypted.");
+ }
+ return clearText;
+ }
+
+ /**
+ * Decrypts previous encrypted/encoded {@link SecureCipher}
+ *
+ * @param encodedCipherText The encoded cipher text
+ * @param callback The callback, when the data is complete.
+ */
+ public void decryptEncodedData(byte[] encodedCipherText, SecureDecryptionCallback callback) {
+ ByteBuffer byteBuffer = ByteBuffer.wrap(encodedCipherText);
+ int encodingTypeVal = byteBuffer.getInt();
+ SecureFileEncodingType encodingType = SecureFileEncodingType.fromId(encodingTypeVal);
+ byte[] encodedEphKey = null;
+ byte[] iv = null;
+ String keyAlias = null;
+ byte[] cipherText = null;
+
+ switch (encodingType) {
+ case EPHEMERAL:
+ int encodedEphKeyLength = byteBuffer.getInt();
+ encodedEphKey = new byte[encodedEphKeyLength];
+ byteBuffer.get(encodedEphKey);
+ case SYMMETRIC:
+ int ivLength = byteBuffer.getInt();
+ iv = new byte[ivLength];
+ byteBuffer.get(iv);
+ case ASYMMETRIC:
+ int keyAliasLength = byteBuffer.getInt();
+ byte[] keyAliasBytes = new byte[keyAliasLength];
+ byteBuffer.get(keyAliasBytes);
+ keyAlias = new String(keyAliasBytes);
+ cipherText = new byte[byteBuffer.remaining()];
+ byteBuffer.get(cipherText);
+ break;
+ case NOT_ENCRYPTED:
+ throw new SecurityException("Cannot determine file type.");
+ }
+ switch (encodingType) {
+ case EPHEMERAL:
+ final byte[] ephemeralCipherText = cipherText;
+ final byte[] ephemeralIv = iv;
+ String finalKeyAlias = keyAlias;
+ decryptSensitiveDataAsymmetric(keyAlias,
+ encodedEphKey,
+ (byte[] decryptedEphKey) -> {
+ if (decryptedEphKey != null) {
+ EphemeralSecretKey ephemeralSecretKey =
+ new EphemeralSecretKey(decryptedEphKey, secureConfig);
+ byte[] decrypted = decryptEphemeralData(
+ ephemeralSecretKey,
+ ephemeralCipherText, ephemeralIv, finalKeyAlias);
+ callback.decryptionComplete(decrypted);
+ ephemeralSecretKey.destroy();
+ } else {
+ Log.i(TAG, "Key was null, this usually means there was an auth " +
+ "failure.");
+ }
+ });
+
+ break;
+ case SYMMETRIC:
+ decryptSensitiveData(
+ keyAlias,
+ cipherText, iv, callback);
+ break;
+ case ASYMMETRIC:
+ decryptSensitiveDataAsymmetric(
+ keyAlias,
+ cipherText, callback);
+ break;
+ case NOT_ENCRYPTED:
+ throw new SecurityException("File not encrypted.");
+ }
+ }
+
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyGenerator.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyGenerator.java
new file mode 100644
index 0000000..09aa5cf
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyGenerator.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.certifications.niap.niapsec.crypto;
+
+import android.os.Build;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import com.android.certifications.niap.niapsec.SecureConfig;
+
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+
+import javax.crypto.KeyGenerator;
+
+/**
+ * Class used for generating ephemeral keys
+ */
+public class SecureKeyGenerator {
+
+ private static final String TAG = "SecureKeyGenerator";
+
+ private SecureConfig secureConfig;
+
+ /**
+ * Create an instance of the key generator with custom settings
+ *
+ * @param secureConfig The config to use when building the key generator
+ * @return The key generator
+ */
+ public static SecureKeyGenerator getInstance(SecureConfig secureConfig) {
+ return new SecureKeyGenerator(secureConfig);
+ }
+
+ private SecureKeyGenerator(SecureConfig secureConfig) {
+ this.secureConfig = secureConfig;
+ }
+
+ /**
+ *
+ * Generates a sensitive data key and adds the SecretKey to the AndroidKeyStore.
+ * Utilizes UnlockedDeviceProtection to ensure that the device must be unlocked in order to
+ * use the generated key.
+ *
+ *
+ * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore.
+ * @return true if the key was generated, false otherwise
+ */
+ public boolean generateKey(String keyAlias) {
+ boolean created = false;
+ try {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(
+ secureConfig.getSymmetricKeyAlgorithm(),
+ secureConfig.getAndroidKeyStore());
+ KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
+ keyAlias, secureConfig.getSymmetricKeyPurposes()).
+ setBlockModes(secureConfig.getSymmetricBlockModes()).
+ setEncryptionPaddings(secureConfig.getSymmetricPaddings()).
+ setKeySize(secureConfig.getSymmetricKeySize());
+ builder = builder.setUserAuthenticationRequired(
+ secureConfig.isSymmetricRequireUserAuthEnabled());
+ builder = builder.setUserAuthenticationValidityDurationSeconds(
+ secureConfig.getSymmetricRequireUserValiditySeconds());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ builder = builder.setUnlockedDeviceRequired(
+ secureConfig.isSymmetricSensitiveDataProtectionEnabled());
+ }
+ keyGenerator.init(builder.build());
+ keyGenerator.generateKey();
+ created = true;
+ } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException |
+ NoSuchProviderException ex) {
+ throw new SecurityException(ex);
+ }
+ return created;
+ }
+
+ /**
+ *
+ * Generates a sensitive data public/private key pair and adds the KeyPair to the AndroidKeyStore.
+ * Utilizes UnlockedDeviceProtection to ensure that the device must be unlocked in order to
+ * use the generated key.
+ *
+ *
+ * ANDROID P ONLY (API LEVEL 28>)
+ *
+ *
+ * @param keyPairAlias The name of the generated SecretKey to save into the AndroidKeyStore.
+ * @return true if the key was generated, false otherwise
+ */
+ public boolean generateAsymmetricKeyPair(String keyPairAlias) {
+ boolean created = false;
+ try {
+ KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(
+ secureConfig.getAsymmetricKeyPairAlgorithm(),
+ secureConfig.getAndroidKeyStore());
+ KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
+ keyPairAlias, secureConfig.getAsymmetricKeyPurposes())
+ .setEncryptionPaddings(secureConfig.getAsymmetricPaddings())
+ .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
+ .setBlockModes(secureConfig.getAsymmetricBlockModes())
+ .setKeySize(secureConfig.getAsymmetricKeySize());
+ builder = builder.setUserAuthenticationRequired(
+ secureConfig.isAsymmetricRequireUserAuthEnabled());
+ builder = builder.setUserAuthenticationValidityDurationSeconds(
+ secureConfig.getAsymmetricRequireUserValiditySeconds());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ builder = builder.setUnlockedDeviceRequired(
+ secureConfig.isAsymmetricSensitiveDataProtectionEnabled());
+ }
+ keyGenerator.initialize(builder.build());
+ keyGenerator.generateKeyPair();
+ created = true;
+ } catch (NoSuchProviderException |
+ InvalidAlgorithmParameterException |
+ NoSuchAlgorithmException ex) {
+ throw new SecurityException(ex);
+ }
+ return created;
+ }
+
+ /**
+ *
+ * Generates an Ephemeral symmetric key that can be fully destroyed and removed from memory.
+ *
+ *
+ * @return The EphemeralSecretKey generated
+ */
+ public EphemeralSecretKey generateEphemeralDataKey() {
+ try {
+ SecureRandom secureRandom = SecureRandom.getInstanceStrong();
+ byte[] key = new byte[secureConfig.getSymmetricKeySize() / 8];
+ secureRandom.nextBytes(key);
+ return new EphemeralSecretKey(key,
+ secureConfig.getSymmetricKeyAlgorithm(),
+ secureConfig);
+ } catch (GeneralSecurityException ex) {
+ throw new SecurityException(ex);
+ }
+ }
+
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyStore.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyStore.java
new file mode 100644
index 0000000..103923e
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/crypto/SecureKeyStore.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.certifications.niap.niapsec.crypto;
+
+import android.security.keystore.KeyInfo;
+
+import com.android.certifications.niap.niapsec.SecureConfig;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+
+/**
+ * Wrapper for the AndroidKeyStore to easily generate keys
+ */
+public class SecureKeyStore {
+
+ private static final String TAG = "SecureKeyStore";
+
+ private SecureConfig secureConfig;
+
+ /**
+ * Gets the key store wrapper with recommended NIAP settings
+ *
+ * @return The key store wrapper
+ */
+ public static SecureKeyStore getDefault(SecureConfig secureConfig) {
+ return new SecureKeyStore(secureConfig);
+ }
+
+ private SecureKeyStore(SecureConfig secureConfig) {
+ this.secureConfig = secureConfig;
+ }
+
+ public boolean keyExists(String keyAlias) {
+ boolean exists = false;
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ exists = keyStore.getCertificate(keyAlias).getPublicKey() != null;
+ } catch (GeneralSecurityException | IOException ex) {
+ throw new SecurityException(ex);
+ }
+ return exists;
+ }
+
+ /**
+ * Checks to see if the specified key is stored in secure hardware
+ *
+ * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore.
+ * @return true if the key is stored in secure hardware
+ */
+ public boolean checkKeyInsideSecureHardware(String keyAlias) {
+ boolean inHardware = false;
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ SecretKey key = (SecretKey) keyStore.getKey(keyAlias, null);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance(key.getAlgorithm(),
+ secureConfig.getAndroidKeyStore());
+ KeyInfo keyInfo;
+ keyInfo = (KeyInfo) factory.getKeySpec(key, KeyInfo.class);
+ inHardware = keyInfo.isInsideSecureHardware();
+ return inHardware;
+ } catch (GeneralSecurityException | IOException e) {
+ return inHardware;
+ }
+ }
+
+ /**
+ * Checks to see if the specified private key is stored in secure hardware
+ *
+ * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore.
+ * @return true if the key is stored in secure hardware
+ */
+ public boolean checkKeyInsideSecureHardwareAsymmetric(String keyAlias) {
+ boolean inHardware = false;
+ try {
+ KeyStore keyStore = KeyStore.getInstance(secureConfig.getAndroidKeyStore());
+ keyStore.load(null);
+ PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, null);
+
+ KeyFactory factory = KeyFactory.getInstance(privateKey.getAlgorithm(),
+ secureConfig.getAndroidKeyStore());
+ KeyInfo keyInfo;
+
+ keyInfo = factory.getKeySpec(privateKey, KeyInfo.class);
+ inHardware = keyInfo.isInsideSecureHardware();
+ return inHardware;
+
+ } catch (GeneralSecurityException | IOException e) {
+ return inHardware;
+ }
+ }
+
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/CertificateValidation.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/CertificateValidation.java
new file mode 100644
index 0000000..d808656
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/CertificateValidation.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.certifications.niap.niapsec.net;
+
+/**
+ * A Configuration Class for the NIAPSEC network library
+ */
+public class CertificateValidation {
+ static boolean enableOCSPStaplingCheck = true;
+ static boolean enableCpvCheck = false;
+ static boolean cpvCheckDoNotFallbackToCrl = false;
+ static boolean enforceTlsV1_2 = true;
+}
\ No newline at end of file
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureKeyManager.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureKeyManager.java
new file mode 100644
index 0000000..7abf24e
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureKeyManager.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec.net;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.security.KeyChainException;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import com.android.certifications.niap.niapsec.SecureConfig;
+
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+
+import javax.net.ssl.X509KeyManager;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Used internally by ValidatableSSLSocketFactory to handle cert chains and keys
+ */
+class SecureKeyManager implements X509KeyManager, KeyChainAliasCallback {
+ private static final String TAG = "SecureKeyManager";
+
+ private final String alias;
+ private final SecureConfig secureConfig;
+
+ private X509Certificate[] certChain;
+ private PrivateKey privateKey;
+ private static Activity ACTIVITY;
+
+ public static void setContext(Activity activity) {
+ ACTIVITY = activity;
+ }
+
+ public enum CertType {
+ X509(0),
+ PKCS12(1),
+ NOT_SUPPORTED(1000);
+
+ private final int type;
+
+ CertType(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public static CertType fromId(int id) {
+ switch (id) {
+ case 0:
+ return X509;
+ case 1:
+ return PKCS12;
+ }
+ return NOT_SUPPORTED;
+ }
+ }
+
+
+ public static SecureKeyManager getDefault(String alias) {
+ return getDefault(alias, SecureConfig.getStrongConfig());
+ }
+
+ public static SecureKeyManager getDefault(String alias, SecureConfig secureConfig) {
+ SecureKeyManager keyManager = new SecureKeyManager(alias, secureConfig);
+ try {
+ KeyChain.choosePrivateKeyAlias(ACTIVITY, keyManager,
+ secureConfig.getClientCertAlgorithms(), null, null, -1, alias);
+ } catch (Exception ex) {
+ Log.i(TAG, "Failed to retrieve client cert: " + ex.getMessage());
+ }
+ return keyManager;
+ }
+
+ public static SecureKeyManager installCertManually(CertType certType,
+ byte[] certData,
+ String keyAlias,
+ SecureConfig secureConfig) {
+ SecureKeyManager keyManager = new SecureKeyManager(keyAlias, secureConfig);
+ Intent intent = KeyChain.createInstallIntent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ switch (certType) {
+ case X509:
+ intent.putExtra(KeyChain.EXTRA_CERTIFICATE, certData);
+ break;
+ case PKCS12:
+ intent.putExtra(KeyChain.EXTRA_PKCS12, certData);
+ break;
+ default:
+ throw new SecurityException("Cert type not supported.");
+ }
+ ACTIVITY.startActivity(intent);
+ return keyManager;
+ }
+
+ public SecureKeyManager(String alias, SecureConfig secureConfig) {
+ this.alias = alias;
+ this.secureConfig = secureConfig;
+ }
+
+ @Override
+ public String chooseClientAlias(String[] arg0, Principal[] arg1, Socket arg2) {
+ return alias;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ if (this.alias.equals(alias)) return certChain;
+ return null;
+ }
+
+ public void setCertChain(X509Certificate[] certChain) {
+ this.certChain = certChain;
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ if (this.alias.equals(alias)) return privateKey;
+ return null;
+ }
+
+ public void setPrivateKey(PrivateKey privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ @Override
+ public final String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final String[] getClientAliases(String keyType, Principal[] issuers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final String[] getServerAliases(String keyType, Principal[] issuers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void alias(@Nullable String alias) {
+ try {
+ assert alias != null;
+ certChain = KeyChain.getCertificateChain(ACTIVITY.getApplicationContext(), alias);
+ privateKey = KeyChain.getPrivateKey(ACTIVITY.getApplicationContext(), alias);
+ if (certChain == null || privateKey == null) {
+ throw new SecurityException("Could not retrieve the cert chain and " +
+ "private key from client cert.");
+ }
+ this.setCertChain(certChain);
+ this.setPrivateKey(privateKey);
+ } catch (KeyChainException | InterruptedException ex) {
+ throw new SecurityException("Could not retrieve the cert chain and " +
+ "private key from client cert.");
+ }
+ }
+}
diff --git a/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureURL.java b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureURL.java
new file mode 100644
index 0000000..64c9c57
--- /dev/null
+++ b/niap-cc/NIAPSEC/src/main/java/com/android/certifications/niap/niapsec/net/SecureURL.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.certifications.niap.niapsec.net;
+
+import android.util.Log;
+
+import com.android.certifications.niap.niapsec.SecureConfig;
+import com.android.certifications.niap.niapsec.config.TldConstants;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.PKIXParameters;
+import java.security.cert.PKIXRevocationChecker;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Wraps URL to provide automatic cert revocation checking through OCSP and enforces TLS use.
+ */
+public class SecureURL {
+
+ private static final String TAG = "SecureURL";
+
+ private final URL url;
+ private final SecureConfig secureConfig;
+ private final String clientCertAlias;
+
+ /**
+ * Creates a SecureURL
+ *
+ * @param spec The URL spec
+ * @param clientCertAlias The cert alias of the client cert to be used (The parameter for KeyChain.choosePrivateKeyAlias), accept null
+ * @throws MalformedURLException If the spec is malformed and not valid URL
+ */
+ public SecureURL(String spec, String clientCertAlias) throws MalformedURLException {
+ this(spec, clientCertAlias, SecureConfig.getStrongConfig());
+ }
+
+ /**
+ * Creates a SecureURL
+ *
+ * @param spec The URL spec
+ * @param clientCertAlias The cert alias of the client cert to be used
+ * @param secureConfig The provided settings to configure TLS version
+ * @throws MalformedURLException If the spec is malformed and not valid URL
+ */
+ public SecureURL(String spec, String clientCertAlias, SecureConfig secureConfig)
+ throws MalformedURLException {
+ this.url = new URL(addProtocol(spec));
+ this.clientCertAlias = clientCertAlias;
+ this.secureConfig = secureConfig;
+ }
+
+ /**
+ * Open the TLS connection to the host specified at construction
+ *
+ * @return The connection that has been established
+ * @throws IOException when there is a bad host name or TLS configuration
+ */
+ public URLConnection openConnection() throws IOException {
+ Log.i(TAG, SecureConfig.PACKAGE_NAME +
+ " initiated a trusted channel to " + getHostname());
+ HttpsURLConnection urlConnection = (HttpsURLConnection) this.url.openConnection();
+ urlConnection.setSSLSocketFactory(new ValidatableSSLSocketFactory(this));
+ Log.i(TAG, "TLS session established and validated to " + getHostInfo());
+ return urlConnection;
+ }
+
+ /**
+ * Checks the hostname against an open SSLSocket connect to the hostname for validity for certs
+ * and hostname validity.
+ *
+ * @param trustedCAs List of trustedCA files to use
+ * @return URLConnection if the SSLSocket has a valid cert and if the hostname is valid
+ */
+ public URLConnection openUserTrustedCertConnection(Map trustedCAs)
+ throws IOException {
+ Log.i(TAG, SecureConfig.PACKAGE_NAME + " initiated a trusted channel to " +
+ getHostname());
+ HttpsURLConnection urlConnection = (HttpsURLConnection) this.url.openConnection();
+ urlConnection.setSSLSocketFactory(
+ new ValidatableSSLSocketFactory(this, trustedCAs, secureConfig));
+ Log.i(TAG, "TLS session established and validated to " + getHostInfo());
+ return urlConnection;
+ }
+
+ /**
+ * Checks the hostname against an open SSLSocket connect to the hostname for validity for certs
+ * and hostname validity.
+ *
+ * @param hostname The host name to check
+ * @param socket The SSLSocket that is open to the URL of the host to check
+ * @return true if the SSLSocket has a valid cert and if the hostname is valid, false otherwise.
+ */
+ public boolean isValid(String hostname, SSLSocket socket) {
+ try {
+ boolean valid = isValid(Arrays.asList(socket.getSession().getPeerCertificates()));
+ boolean hostnameValid = HttpsURLConnection.getDefaultHostnameVerifier()
+ .verify(hostname, socket.getSession());
+ if(!valid) {
+ Log.i(TAG, "Failed to validate X509v3 certificates. "+ getHostInfo());
+ } else if(!hostnameValid) {
+ Log.i(TAG, "Failed to validate presented identifier. "+getHostInfo());
+ }
+ return hostnameValid
+ && valid
+ && validTldWildcards(Arrays.asList(socket.getSession().getPeerCertificates()));
+ } catch (SSLPeerUnverifiedException e) {
+ Log.i(TAG, "Validity Check failed: "+ getHostInfo() + " Ex:" + e.getMessage());
+ //e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * Checks the HttpsUrlConnection certificates for validity.
+ *