Subtree merged in MemorizingTrustManager

This commit is contained in:
Sam Whited 2014-10-28 12:11:51 -04:00
commit 41d2e72be7
35 changed files with 1875 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit fad835037adc1bd313bb56b694426fca4eb67346

11
libs/MemorizingTrustManager/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
bin
build
gen
local.properties
example/bin
example/gen
tags
.project
.classpath
.gradle
.*.swp

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.duenndns.ssl"
android:versionCode="1"
android:versionName="1.0">
<application android:label="MemorizingTrustManager">
<activity android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
</application>
</manifest>

View File

@ -0,0 +1,21 @@
The MIT license.
Copyright (c) 2010 Georg Lukas <georg@op-co.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,125 @@
# MemorizingTrustManager - Private Cloud Support for Your App
MemorizingTrustManager (MTM) is a project to enable smarter and more secure use
of SSL on Android. If it encounters an unknown SSL certificate, it asks the
user whether to accept the certificate once, permanently or to abort the
connection. This is a step in preventing man-in-the-middle attacks by blindly
accepting any invalid, self-signed and/or expired certificates.
MTM is aimed at providing seamless integration into your Android application,
and the source code is available under the MIT license.
## Screenshots
![MemorizingTrustManager dialog](mtm-screenshot.png)
![MemorizingTrustManager notification](mtm-notification.png)
![MemorizingTrustManager server name dialog](mtm-servername.png)
## Status
MemorizingTrustManager is in production use in the
[yaxim XMPP client](https://yaxim.org/). It is usable and easy to integrate,
though it does not yet support hostname validation (the Java API makes it
**hard** to integrate).
## Integration
MTM is easy to integrate into your own application. Follow these steps or have
a look into the demo application in the `example` directory.
### 1. Add MTM to your project
Download the MTM source from GitHub, or add it as a
[git submodule](http://git-scm.com/docs/git-submodule):
# plain download:
git clone https://github.com/ge0rg/MemorizingTrustManager
# submodule:
git submodule add https://github.com/ge0rg/MemorizingTrustManager
Then add a library project dependency to `default.properties`:
android.library.reference.1=MemorizingTrustManager
### 2. Add the MTM (popup) Activity to your manifest
Edit your `AndroidManifest.xml` and add the MTM activity element right before the
end of your closing `</application>` tag.
...
<activity android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
/>
</application>
</manifest>
### 3. Hook MTM as the default TrustManager for your connection type
Hooking MemorizingTrustmanager in HTTPS connections:
// register MemorizingTrustManager for HTTPS
SSLContext sc = SSLContext.getInstance("TLS");
MemorizingTrustManager mtm = new MemorizingTrustManager(this);
sc.init(null, new X509TrustManager[] { mtm }, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(
mtm.wrapHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier()));
Or, for aSmack you can use `setCustomSSLContext()`:
org.jivesoftware.smack.ConnectionConfiguration connectionConfiguration = …
SSLContext sc = SSLContext.getInstance("TLS");
MemorizingTrustManager mtm = new MemorizingTrustManager(this);
sc.init(null, new X509TrustManager[] { mtm }, new java.security.SecureRandom());
connectionConfiguration.setCustomSSLContext(sc);
connectionConfiguration.setHostnameVerifier(
mtm.wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier()));
By default, MTM falls back to the system `TrustManager` before asking the user.
If you do not trust the establishment, you can enforce a dialog on *every new
connection* by supplying a `defaultTrustManager = null` parameter to the
constructor:
MemorizingTrustManager mtm = new MemorizingTrustManager(this, null);
If you want to use a different underlying `TrustManager`, like
[AndroidPinning](https://github.com/moxie0/AndroidPinning), just supply that to
MTM's constructor:
X509TrustManager pinning = new PinningTrustManager(SystemKeyStore.getInstance(),
new String[] {"f30012bbc18c231ac1a44b788e410ce754182513"}, 0);
MemorizingTrustManager mtm = new MemorizingTrustManager(this, pinning);
### 4. Profit!
### Logging
MTM uses java.util.logging (JUL) for logging purposes. If you have not
configured a Handler for JUL, then Android will by default log all
messages of Level.INFO or higher. In order to get also the debug log
messages (those with Level.FINE or lower) you need to configure a
Handler accordingly. The MTM example project contains
de.duenndns.mtmexample.JULHandler, which allows to enable and disable
debug logging at runtime.
## Alternatives
MemorizingTrustManager is not the only one out there.
[**NetCipher**](https://guardianproject.info/code/netcipher/) is an Android
library made by the [Guardian Project](https://guardianproject.info/) to
improve network security for mobile apps. It comes with a StrongTrustManager
to do more thorough certificate checks, an independent Root CA store, and code
to easily route your traffic through
[the Tor network](https://www.torproject.org/) using [Orbot](https://guardianproject.info/apps/orbot/).
[**AndroidPinning**](https://github.com/moxie0/AndroidPinning) is another Android
library, written by [Moxie Marlinspike](http://www.thoughtcrime.org/) to allow
pinning of server certificates, improving security against government-scale
MitM attacks. Use this if your app is made to communicate with a specific
server!
## Contribute
Please [help translating MTM into more languages](https://translations.launchpad.net/yaxim/master/+pots/mtm/)!

View File

@ -0,0 +1,17 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked in Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.

View File

@ -0,0 +1,32 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.7.+'
}
}
apply plugin: 'android-library'
android {
compileSdkVersion 19
buildToolsVersion "19.1"
defaultConfig {
minSdkVersion 7
targetSdkVersion 19
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
}

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="MemorizingTrustManager" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.duenndns.mtmexample"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk
android:minSdkVersion="3"
android:targetSdkVersion="19" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="@string/app_name" android:icon="@android:drawable/ic_lock_lock">
<activity
android:name=".MTMExample"
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- ADD THE FOLLOWING TO YOUR MANIFEST: -->
<activity android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
</application>
</manifest>

View File

@ -0,0 +1,18 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked in Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.
application.package=de.duenndns.mtmexample

View File

@ -0,0 +1,23 @@
apply plugin: 'android'
dependencies {
compile rootProject
}
android {
compileSdkVersion 19
buildToolsVersion "19.1"
defaultConfig {
minSdkVersion 7
targetSdkVersion 19
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
res.srcDirs = ['res']
}
}
}

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="MTMExample" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

View File

@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@ -0,0 +1,12 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "ant.properties", and override values to adapt the script to your
# project structure.
android.library.reference.1=../
# Project target.
target=android-19

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<EditText
android:id="@+id/url"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="HTTPS address"
android:text="https://op-co.de/mtm/"
android:singleLine="true"
/>
<Button
android:id="@+id/connect"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Connect"
/>
<TextView
android:id="@+id/content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Please enter a HTTPS URL and press 'Connect'!"
android:textSize="11pt"
/>
<Button
android:id="@+id/manage"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Clean up Certificates"
android:onClick="onManage"
/>
</LinearLayout>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MemorizingTrustManager Example</string>
</resources>

View File

@ -0,0 +1,169 @@
package de.duenndns.mtmexample;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringBufferInputStream;
import java.io.StringWriter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import android.util.Log;
/**
* A <code>java.util.logging</code> (JUL) Handler for Android.
* <p>
* If you want fine-grained control over MTM's logging, you can copy this
* class to your code base and call the static {@link #initialize()} method.
* </p>
* <p>
* This JUL Handler passes log messages sent to JUL to the Android log, while
* keeping the format and stack traces of optionally supplied Exceptions. It
* further allows to install a {@link DebugLogSettings} class via
* {@link #setDebugLogSettings(DebugLogSettings)} that determines whether JUL log messages of
* level {@link java.util.logging.Level#FINE} or lower are logged. This gives
* the application developer more control over the logged messages, while
* allowing a library developer to place debug log messages without risking to
* spam the Android log.
* </p>
* <p>
* If there are no {@code DebugLogSettings} configured, then all messages sent
* to JUL will be logged.
* </p>
*
* @author Florian Schmaus
*
*/
@SuppressWarnings("deprecation")
public class JULHandler extends Handler {
/** Implement this interface to toggle debug logging.
*/
public interface DebugLogSettings {
public boolean isDebugLogEnabled();
}
private static final String CLASS_NAME = JULHandler.class.getName();
/**
* The global LogManager configuration.
* <p>
* This configures:
* <ul>
* <li> JULHandler as the default handler for all log messages
* <li> A default log level FINEST (300). Meaning that log messages of a level 300 or higher a
* logged
* </ul>
* </p>
*/
private static final InputStream LOG_MANAGER_CONFIG = new StringBufferInputStream(
// @formatter:off
"handlers = " + CLASS_NAME + '\n' +
".level = FINEST"
);
// @formatter:on
// Constants for Android vs. JUL debug level comparisons
private static final int FINE_INT = Level.FINE.intValue();
private static final int INFO_INT = Level.INFO.intValue();
private static final int WARN_INT = Level.WARNING.intValue();
private static final int SEVE_INT = Level.SEVERE.intValue();
private static final Logger LOGGER = Logger.getLogger(CLASS_NAME);
/** A formatter that creates output similar to Android's Log.x. */
private static final Formatter FORMATTER = new Formatter() {
@Override
public String format(LogRecord logRecord) {
Throwable thrown = logRecord.getThrown();
if (thrown != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, false);
pw.write(logRecord.getMessage() + ' ');
thrown.printStackTrace(pw);
pw.flush();
return sw.toString();
} else {
return logRecord.getMessage();
}
}
};
private static DebugLogSettings sDebugLogSettings;
private static boolean initialized = false;
public static void initialize() {
try {
LogManager.getLogManager().readConfiguration(LOG_MANAGER_CONFIG);
initialized = true;
} catch (IOException e) {
Log.e("JULHandler", "Can not initialize configuration", e);
}
if (initialized) LOGGER.info("Initialzied java.util.logging logger");
}
public static void setDebugLogSettings(DebugLogSettings debugLogSettings) {
if (!isInitialized()) initialize();
sDebugLogSettings = debugLogSettings;
}
public static boolean isInitialized() {
return initialized;
}
public JULHandler() {
setFormatter(FORMATTER);
}
@Override
public void close() {}
@Override
public void flush() {}
@Override
public boolean isLoggable(LogRecord record) {
final boolean debugLog = sDebugLogSettings == null ? true : sDebugLogSettings
.isDebugLogEnabled();
if (record.getLevel().intValue() <= FINE_INT) {
return debugLog;
}
return true;
}
/** JUL method that forwards log records to Android's LogCat. */
@Override
public void publish(LogRecord record) {
if (!isLoggable(record)) return;
final int priority = getAndroidPriority(record.getLevel());
final String tag = substringAfterLastDot(record.getSourceClassName());
final String msg = getFormatter().format(record);
Log.println(priority, tag, msg);
}
/** Helper to convert JUL verbosity levels to Android's Log. */
private static int getAndroidPriority(Level level) {
int value = level.intValue();
if (value >= SEVE_INT) {
return Log.ERROR;
} else if (value >= WARN_INT) {
return Log.WARN;
} else if (value >= INFO_INT) {
return Log.INFO;
} else {
return Log.DEBUG;
}
}
/** Helper to extract short class names. */
private static String substringAfterLastDot(String s) {
return s.substring(s.lastIndexOf('.') + 1).trim();
}
}

View File

@ -0,0 +1,143 @@
package de.duenndns.mtmexample;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.TextView;
import java.net.URL;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Collections;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.X509TrustManager;
import de.duenndns.ssl.MemorizingTrustManager;
/**
* Example to demonstrate the use of MemorizingTrustManager on HTTPS
* sockets.
*/
public class MTMExample extends Activity implements OnClickListener
{
MemorizingTrustManager mtm;
TextView content;
HostnameVerifier defaultverifier;
EditText urlinput;
String text;
Handler hdlr;
/** Creates the Activity and registers a MemorizingTrustManager. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
JULHandler.initialize();
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.mtmexample);
// set up gui elements
findViewById(R.id.connect).setOnClickListener(this);
content = (TextView)findViewById(R.id.content);
urlinput = (EditText)findViewById(R.id.url);
// register handler for background thread
hdlr = new Handler();
// Here, the MemorizingTrustManager is activated for HTTPS
try {
// set location of the keystore
MemorizingTrustManager.setKeyStoreFile("private", "sslkeys.bks");
// register MemorizingTrustManager for HTTPS
SSLContext sc = SSLContext.getInstance("TLS");
mtm = new MemorizingTrustManager(this);
sc.init(null, new X509TrustManager[] { mtm },
new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(
mtm.wrapHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier()));
// disable redirects to reduce possible confusion
HttpsURLConnection.setFollowRedirects(false);
} catch (Exception e) {
e.printStackTrace();
}
}
/** Updates the screen content from a background thread. */
void setText(final String s, final boolean progress) {
text = s;
hdlr.post(new Runnable() {
public void run() {
content.setText(s);
setProgressBarIndeterminateVisibility(progress);
}
});
}
/** Spawns a new thread connecting to the specified URL.
* The result of the request is displayed on the screen.
* @param urlString a HTTPS URL to connect to.
*/
void connect(final String urlString) {
new Thread() {
public void run() {
try {
URL u = new URL(urlString);
HttpsURLConnection c = (HttpsURLConnection)u.openConnection();
c.connect();
setText("" + c.getResponseCode() + " "
+ c.getResponseMessage(), false);
c.disconnect();
} catch (Exception e) {
setText(e.toString(), false);
e.printStackTrace();
}
}
}.start();
}
/** Reacts on the connect Button press. */
@Override
public void onClick(View view) {
String url = urlinput.getText().toString();
setText("Loading " + url, true);
setProgressBarIndeterminateVisibility(true);
connect(url);
}
/** React on the "Manage Certificates" button press. */
public void onManage(View view) {
final ArrayList<String> aliases = Collections.list(mtm.getCertificates());
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.select_dialog_item, aliases);
new AlertDialog.Builder(this).setTitle("Tap Certificate to Delete")
.setNegativeButton(android.R.string.cancel, null)
.setAdapter(adapter, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
String alias = aliases.get(which);
mtm.deleteCertificate(alias);
setText("Deleted " + alias, false);
} catch (KeyStoreException e) {
e.printStackTrace();
setText("Error: " + e.getLocalizedMessage(), false);
}
}
})
.create().show();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@ -0,0 +1,12 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "ant.properties", and override values to adapt the script to your
# project structure.
android.library=true
# Project target.
target=android-19

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mtm_accept_cert">Unbekanntes Zertifikat akzeptieren?</string>
<string name="mtm_trust_anchor">Das Serverzertifikat stammt nicht von einer bekannten Ausstellungsstelle (CA).</string>
<string name="mtm_cert_expired">The server certificate is expired.</string>
<string name="mtm_accept_servername">Abweichenden Servernamen akzeptieren?</string>
<string name="mtm_hostname_mismatch">Der Server konnte sich nicht als \"%s\" ausweisen. Das Zertifikat gilt nur für:</string>
<string name="mtm_connect_anyway">Verbindung trotzdem aufbauen?</string>
<string name="mtm_cert_details">Zertifikat-Details:</string>
<string name="mtm_decision_always">Immer</string>
<string name="mtm_decision_once">Einmal</string>
<string name="mtm_decision_abort">Abbrechen</string>
<string name="mtm_notification">Zertifikatsprüfung</string>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mtm_accept_cert">¿Aceptar certicado desconocido?</string>
<string name="mtm_trust_anchor">El certificado del servidor no está firmado por una Autoridad Conocida (CA).</string>
<string name="mtm_cert_expired">The server certificate is expired.</string>
<string name="mtm_accept_servername">¿Aceptar discordancia en nombre del servidor?</string>
<string name="mtm_hostname_mismatch">El servidor no ha podido autenticarte como \"%s\". El certificado es solo válido para:</string>
<string name="mtm_connect_anyway">¿Quieres conectar de todas formas?</string>
<string name="mtm_cert_details">Detalle del certificado:</string>
<string name="mtm_decision_always">Siempre</string>
<string name="mtm_decision_once">Una vez</string>
<string name="mtm_decision_abort">Abortar</string>
<string name="mtm_notification">Verificación de Certificado</string>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mtm_accept_cert">Ziurtagiri ezezaguna onartu?</string>
<string name="mtm_trust_anchor">Zerbitzariaren ziurtagiria ez dago Ziurtagiri-emaile Autoritate ezagun batez sinatuta.</string>
<string name="mtm_cert_expired">Zerbitzariaren ziurtagiria iraungi da.</string>
<string name="mtm_accept_servername">Zerbitzariaren izeneko desadostasuna onartu?</string>
<string name="mtm_hostname_mismatch">Zerbitzaria ezin izan da \&quot;%s\&quot; bezala autentifikatu. Ziurtagiria soilik honetarako baliagarria da:</string>
<string name="mtm_connect_anyway">Konektatu hala ere?</string>
<string name="mtm_cert_details">Ziurtagiriaren xehetasunak:</string>
<string name="mtm_decision_always">Beti</string>
<string name="mtm_decision_once">Behin</string>
<string name="mtm_decision_abort">Utzi</string>
<string name="mtm_notification">Ziurtagiriaren egiaztapena</string>
</resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mtm_accept_cert">Hyväksytäänkö palvelimen antama tuntematon varmenne?</string>
<string name="mtm_trust_anchor">Palvelimen varmenne ei ole tunnetun varmentajan (CA) allekirjoittama.</string>
<string name="mtm_accept_servername">Sallitaanko palvelimen nimi, joka ei vastaa varmeenteessa olevaa nimeä?</string>
<string name="mtm_hostname_mismatch">Palvelimella ei ole varmennetta nimelle \"%s\". Varmenteen sisältämät nimet:</string>
<string name="mtm_connect_anyway">Haluatko jatkaa yhteyden muodostamista?</string>
<string name="mtm_cert_details">Sertifikaatin tiedot:</string>
<string name="mtm_decision_always">Aina</string>
<string name="mtm_decision_once">Kerran</string>
<string name="mtm_decision_abort">Keskeytä</string>
<string name="mtm_notification">Varmenteen tarkistus</string>
</resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mtm_accept_cert">Accept Unknown Certificate?</string>
<string name="mtm_trust_anchor">Le certificat du serveur nest pas signé par une Autorité de Certification reconnue.</string>
<string name="mtm_accept_servername">Accept Mismatching Server Name?</string>
<string name="mtm_hostname_mismatch">Server could not authenticate as \"%s\". The certificate is only valid for:</string>
<string name="mtm_connect_anyway">Do you want to connect anyway?</string>
<string name="mtm_cert_details">Détails du certificat:</string>
<string name="mtm_decision_always">Toujours</string>
<string name="mtm_decision_once">Une seule fois</string>
<string name="mtm_decision_abort">Annuler</string>
<string name="mtm_notification">Certificate Verification</string>
</resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mtm_accept_cert">Godta ukjent sertifikat?</string>
<string name="mtm_trust_anchor">Sertifikatet er ikke utstilt av en kjent utstiller (CA).</string>
<string name="mtm_accept_servername">Godta feil servernavn?</string>
<string name="mtm_hostname_mismatch">Serveren heter ikke \"%s\". Sertifikatet gjelder bare for: </string>
<string name="mtm_connect_anyway">Vil du bruke serveren likevel?</string>
<string name="mtm_cert_details">Sertifikatdetaljer:</string>
<string name="mtm_decision_always">Alltid</string>
<string name="mtm_decision_once">En gang</string>
<string name="mtm_decision_abort">Avbryt</string>
<string name="mtm_notification">Sertifikat-sjekk</string>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mtm_accept_cert">Accept Unknown Certificate?</string>
<string name="mtm_trust_anchor">The server certificate is not signed by a known Certificate Authority.</string>
<string name="mtm_cert_expired">The server certificate is expired.</string>
<string name="mtm_accept_servername">Accept Mismatching Server Name?</string>
<string name="mtm_hostname_mismatch">Server could not authenticate as \&quot;%s\&quot;. The certificate is only valid for:</string>
<string name="mtm_connect_anyway">Do you want to connect anyway?</string>
<string name="mtm_cert_details">Certificate details:</string>
<string name="mtm_decision_always">Always</string>
<string name="mtm_decision_once">Once</string>
<string name="mtm_decision_abort">Abort</string>
<string name="mtm_notification">Certificate Verification</string>
</resources>

View File

@ -0,0 +1 @@
include ':example'

View File

@ -0,0 +1,33 @@
/* MemorizingTrustManager - a TrustManager which asks the user about invalid
* certificates and memorizes their decision.
*
* Copyright (c) 2010 Georg Lukas <georg@op-co.de>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.duenndns.ssl;
class MTMDecision {
public final static int DECISION_INVALID = 0;
public final static int DECISION_ABORT = 1;
public final static int DECISION_ONCE = 2;
public final static int DECISION_ALWAYS = 3;
int state = DECISION_INVALID;
}

View File

@ -0,0 +1,103 @@
/* MemorizingTrustManager - a TrustManager which asks the user about invalid
* certificates and memorizes their decision.
*
* Copyright (c) 2010 Georg Lukas <georg@op-co.de>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.duenndns.ssl;
import java.util.logging.Level;
import java.util.logging.Logger;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.*;
import android.content.Intent;
import android.os.Bundle;
public class MemorizingActivity extends Activity
implements OnClickListener,OnCancelListener {
private final static Logger LOGGER = Logger.getLogger(MemorizingActivity.class.getName());
int decisionId;
AlertDialog dialog;
@Override
public void onCreate(Bundle savedInstanceState) {
LOGGER.log(Level.FINE, "onCreate");
super.onCreate(savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
Intent i = getIntent();
decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert);
String cert = i.getStringExtra(MemorizingTrustManager.DECISION_INTENT_CERT);
LOGGER.log(Level.FINE, "onResume with " + i.getExtras() + " decId=" + decisionId + " data: " + i.getData());
dialog = new AlertDialog.Builder(this).setTitle(titleId)
.setMessage(cert)
.setPositiveButton(R.string.mtm_decision_always, this)
.setNeutralButton(R.string.mtm_decision_once, this)
.setNegativeButton(R.string.mtm_decision_abort, this)
.setOnCancelListener(this)
.create();
dialog.show();
}
@Override
protected void onPause() {
if (dialog.isShowing())
dialog.dismiss();
super.onPause();
}
void sendDecision(int decision) {
LOGGER.log(Level.FINE, "Sending decision: " + decision);
MemorizingTrustManager.interactResult(decisionId, decision);
finish();
}
// react on AlertDialog button press
public void onClick(DialogInterface dialog, int btnId) {
int decision;
dialog.dismiss();
switch (btnId) {
case DialogInterface.BUTTON_POSITIVE:
decision = MTMDecision.DECISION_ALWAYS;
break;
case DialogInterface.BUTTON_NEUTRAL:
decision = MTMDecision.DECISION_ONCE;
break;
default:
decision = MTMDecision.DECISION_ABORT;
}
sendDecision(decision);
}
public void onCancel(DialogInterface dialog) {
sendDecision(MTMDecision.DECISION_ABORT);
}
}

View File

@ -0,0 +1,735 @@
/* MemorizingTrustManager - a TrustManager which asks the user about invalid
* certificates and memorizes their decision.
*
* Copyright (c) 2010 Georg Lukas <georg@op-co.de>
*
* MemorizingTrustManager.java contains the actual trust manager and interface
* code to create a MemorizingActivity and obtain the results.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.duenndns.ssl;
import android.app.Activity;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.SparseArray;
import android.os.Handler;
import java.io.File;
import java.io.IOException;
import java.security.cert.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
/**
* A X509 trust manager implementation which asks the user about invalid
* certificates and memorizes their decision.
* <p>
* The certificate validity is checked using the system default X509
* TrustManager, creating a query Dialog if the check fails.
* <p>
* <b>WARNING:</b> This only works if a dedicated thread is used for
* opening sockets!
*/
public class MemorizingTrustManager implements X509TrustManager {
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
private final static int NOTIFICATION_ID = 100509;
final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
static String KEYSTORE_DIR = "KeyStore";
static String KEYSTORE_FILE = "KeyStore.bks";
Context master;
Activity foregroundAct;
NotificationManager notificationManager;
private static int decisionId = 0;
private static SparseArray<MTMDecision> openDecisions = new SparseArray<MTMDecision>();
Handler masterHandler;
private File keyStoreFile;
private KeyStore appKeyStore;
private X509TrustManager defaultTrustManager;
private X509TrustManager appTrustManager;
/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
*
* You need to supply the application context. This has to be one of:
* - Application
* - Activity
* - Service
*
* The context is used for file management, to display the dialog /
* notification and for obtaining translated strings.
*
* @param m Context for the application.
* @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate.
*/
public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) {
init(m);
this.appTrustManager = getTrustManager(appKeyStore);
this.defaultTrustManager = defaultTrustManager;
}
/** Creates an instance of the MemorizingTrustManager class using the system X509TrustManager.
*
* You need to supply the application context. This has to be one of:
* - Application
* - Activity
* - Service
*
* The context is used for file management, to display the dialog /
* notification and for obtaining translated strings.
*
* @param m Context for the application.
*/
public MemorizingTrustManager(Context m) {
init(m);
this.appTrustManager = getTrustManager(appKeyStore);
this.defaultTrustManager = getTrustManager(null);
}
void init(Context m) {
master = m;
masterHandler = new Handler(m.getMainLooper());
notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
Application app;
if (m instanceof Application) {
app = (Application)m;
} else if (m instanceof Service) {
app = ((Service)m).getApplication();
} else if (m instanceof Activity) {
app = ((Activity)m).getApplication();
} else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
appKeyStore = loadAppKeyStore();
}
/**
* Returns a X509TrustManager list containing a new instance of
* TrustManagerFactory.
*
* This function is meant for convenience only. You can use it
* as follows to integrate TrustManagerFactory for HTTPS sockets:
*
* <pre>
* SSLContext sc = SSLContext.getInstance("TLS");
* sc.init(null, MemorizingTrustManager.getInstanceList(this),
* new java.security.SecureRandom());
* HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
* </pre>
* @param c Activity or Service to show the Dialog / Notification
*/
public static X509TrustManager[] getInstanceList(Context c) {
return new X509TrustManager[] { new MemorizingTrustManager(c) };
}
/**
* Binds an Activity to the MTM for displaying the query dialog.
*
* This is useful if your connection is run from a service that is
* triggered by user interaction -- in such cases the activity is
* visible and the user tends to ignore the service notification.
*
* You should never have a hidden activity bound to MTM! Use this
* function in onResume() and @see unbindDisplayActivity in onPause().
*
* @param act Activity to be bound
*/
public void bindDisplayActivity(Activity act) {
foregroundAct = act;
}
/**
* Removes an Activity from the MTM display stack.
*
* Always call this function when the Activity added with
* {@link #bindDisplayActivity(Activity)} is hidden.
*
* @param act Activity to be unbound
*/
public void unbindDisplayActivity(Activity act) {
// do not remove if it was overridden by a different activity
if (foregroundAct == act)
foregroundAct = null;
}
/**
* Changes the path for the KeyStore file.
*
* The actual filename relative to the app's directory will be
* <code>app_<i>dirname</i>/<i>filename</i></code>.
*
* @param dirname directory to store the KeyStore.
* @param filename file name for the KeyStore.
*/
public static void setKeyStoreFile(String dirname, String filename) {
KEYSTORE_DIR = dirname;
KEYSTORE_FILE = filename;
}
/**
* Get a list of all certificate aliases stored in MTM.
*
* @return an {@link Enumeration} of all certificates
*/
public Enumeration<String> getCertificates() {
try {
return appKeyStore.aliases();
} catch (KeyStoreException e) {
// this should never happen, however...
throw new RuntimeException(e);
}
}
/**
* Get a certificate for a given alias.
*
* @param alias the certificate's alias as returned by {@link #getCertificates()}.
*
* @return the certificate associated with the alias or <tt>null</tt> if none found.
*/
public Certificate getCertificate(String alias) {
try {
return appKeyStore.getCertificate(alias);
} catch (KeyStoreException e) {
// this should never happen, however...
throw new RuntimeException(e);
}
}
/**
* Removes the given certificate from MTMs key store.
*
* <p>
* <b>WARNING</b>: this does not immediately invalidate the certificate. It is
* well possible that (a) data is transmitted over still existing connections or
* (b) new connections are created using TLS renegotiation, without a new cert
* check.
* </p>
* @param alias the certificate's alias as returned by {@link #getCertificates()}.
*
* @throws KeyStoreException if the certificate could not be deleted.
*/
public void deleteCertificate(String alias) throws KeyStoreException {
appKeyStore.deleteEntry(alias);
keyStoreUpdated();
}
/**
* Creates a new hostname verifier supporting user interaction.
*
* <p>This method creates a new {@link HostnameVerifier} that is bound to
* the given instance of {@link MemorizingTrustManager}, and leverages an
* existing {@link HostnameVerifier}. The returned verifier performs the
* following steps, returning as soon as one of them succeeds:
* </p>
* <ol>
* <li>Success, if the wrapped defaultVerifier accepts the certificate.</li>
* <li>Success, if the server certificate is stored in the keystore under the given hostname.</li>
* <li>Ask the user and return accordingly.</li>
* <li>Failure on exception.</li>
* </ol>
*
* @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check
* @return a new hostname verifier using the MTM's key store
*
* @throws IllegalArgumentException if the defaultVerifier parameter is null
*/
public HostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier) {
if (defaultVerifier == null)
throw new IllegalArgumentException("The default verifier may not be null");
return new MemorizingHostnameVerifier(defaultVerifier);
}
public HostnameVerifier wrapHostnameVerifierNonInteractive(final HostnameVerifier defaultVerifier) {
if (defaultVerifier == null)
throw new IllegalArgumentException("The default verifier may not be null");
return new NonInteractiveMemorizingHostnameVerifier(defaultVerifier);
}
X509TrustManager getTrustManager(KeyStore ks) {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(ks);
for (TrustManager t : tmf.getTrustManagers()) {
if (t instanceof X509TrustManager) {
return (X509TrustManager)t;
}
}
} catch (Exception e) {
// Here, we are covering up errors. It might be more useful
// however to throw them out of the constructor so the
// embedding app knows something went wrong.
LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e);
}
return null;
}
KeyStore loadAppKeyStore() {
KeyStore ks;
try {
ks = KeyStore.getInstance(KeyStore.getDefaultType());
} catch (KeyStoreException e) {
LOGGER.log(Level.SEVERE, "getAppKeyStore()", e);
return null;
}
try {
ks.load(null, null);
ks.load(new java.io.FileInputStream(keyStoreFile), "MTM".toCharArray());
} catch (java.io.FileNotFoundException e) {
LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e);
}
return ks;
}
void storeCert(String alias, Certificate cert) {
try {
appKeyStore.setCertificateEntry(alias, cert);
} catch (KeyStoreException e) {
LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e);
return;
}
keyStoreUpdated();
}
void storeCert(X509Certificate cert) {
storeCert(cert.getSubjectDN().toString(), cert);
}
void keyStoreUpdated() {
// reload appTrustManager
appTrustManager = getTrustManager(appKeyStore);
// store KeyStore to file
java.io.FileOutputStream fos = null;
try {
fos = new java.io.FileOutputStream(keyStoreFile);
appKeyStore.store(fos, "MTM".toCharArray());
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
}
}
}
}
// if the certificate is stored in the app key store, it is considered "known"
private boolean isCertKnown(X509Certificate cert) {
try {
return appKeyStore.getCertificateAlias(cert) != null;
} catch (KeyStoreException e) {
return false;
}
}
private boolean isExpiredException(Throwable e) {
do {
if (e instanceof CertificateExpiredException)
return true;
e = e.getCause();
} while (e != null);
return false;
}
public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive)
throws CertificateException
{
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
try {
LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
if (isServer)
appTrustManager.checkServerTrusted(chain, authType);
else
appTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException ae) {
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
// if the cert is stored in our appTrustManager, we ignore expiredness
if (isExpiredException(ae)) {
LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore");
return;
}
if (isCertKnown(chain[0])) {
LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
return;
}
try {
if (defaultTrustManager == null)
throw ae;
LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
if (isServer)
defaultTrustManager.checkServerTrusted(chain, authType);
else
defaultTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) {
e.printStackTrace();
if (interactive) {
interactCert(chain, authType, e);
} else {
throw e;
}
}
}
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
checkCertTrusted(chain, authType, false,true);
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
checkCertTrusted(chain, authType, true,true);
}
public X509Certificate[] getAcceptedIssuers()
{
LOGGER.log(Level.FINE, "getAcceptedIssuers()");
return defaultTrustManager.getAcceptedIssuers();
}
private int createDecisionId(MTMDecision d) {
int myId;
synchronized(openDecisions) {
myId = decisionId;
openDecisions.put(myId, d);
decisionId += 1;
}
return myId;
}
private static String hexString(byte[] data) {
StringBuffer si = new StringBuffer();
for (int i = 0; i < data.length; i++) {
si.append(String.format("%02x", data[i]));
if (i < data.length - 1)
si.append(":");
}
return si.toString();
}
private static String certHash(final X509Certificate cert, String digest) {
try {
MessageDigest md = MessageDigest.getInstance(digest);
md.update(cert.getEncoded());
return hexString(md.digest());
} catch (java.security.cert.CertificateEncodingException e) {
return e.getMessage();
} catch (java.security.NoSuchAlgorithmException e) {
return e.getMessage();
}
}
private void certDetails(StringBuffer si, X509Certificate c) {
SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd");
si.append("\n");
si.append(c.getSubjectDN().toString());
si.append("\n");
si.append(validityDateFormater.format(c.getNotBefore()));
si.append(" - ");
si.append(validityDateFormater.format(c.getNotAfter()));
si.append("\nSHA-256: ");
si.append(certHash(c, "SHA-256"));
si.append("\nSHA-1: ");
si.append(certHash(c, "SHA-1"));
si.append("\nSigned by: ");
si.append(c.getIssuerDN().toString());
si.append("\n");
}
private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
Throwable e = cause;
LOGGER.log(Level.FINE, "certChainMessage for " + e);
StringBuffer si = new StringBuffer();
if (e.getCause() != null) {
e = e.getCause();
// HACK: there is no sane way to check if the error is a "trust anchor
// not found", so we use string comparison.
if (NO_TRUST_ANCHOR.equals(e.getMessage())) {
si.append(master.getString(R.string.mtm_trust_anchor));
} else
si.append(e.getLocalizedMessage());
si.append("\n");
}
si.append("\n");
si.append(master.getString(R.string.mtm_connect_anyway));
si.append("\n\n");
si.append(master.getString(R.string.mtm_cert_details));
for (X509Certificate c : chain) {
certDetails(si, c);
}
return si.toString();
}
private String hostNameMessage(X509Certificate cert, String hostname) {
StringBuffer si = new StringBuffer();
si.append(master.getString(R.string.mtm_hostname_mismatch, hostname));
si.append("\n\n");
try {
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
if (sans == null) {
si.append(cert.getSubjectDN());
si.append("\n");
} else for (List<?> altName : sans) {
Object name = altName.get(1);
if (name instanceof String) {
si.append("[");
si.append((Integer)altName.get(0));
si.append("] ");
si.append(name);
si.append("\n");
}
}
} catch (CertificateParsingException e) {
e.printStackTrace();
si.append("<Parsing error: ");
si.append(e.getLocalizedMessage());
si.append(">\n");
}
si.append("\n");
si.append(master.getString(R.string.mtm_connect_anyway));
si.append("\n\n");
si.append(master.getString(R.string.mtm_cert_details));
certDetails(si, cert);
return si.toString();
}
// We can use Notification.Builder once MTM's minSDK is >= 11
@SuppressWarnings("deprecation")
void startActivityNotification(Intent intent, int decisionId, String certName) {
Notification n = new Notification(android.R.drawable.ic_lock_lock,
master.getString(R.string.mtm_notification),
System.currentTimeMillis());
PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
n.setLatestEventInfo(master.getApplicationContext(),
master.getString(R.string.mtm_notification),
certName, call);
n.flags |= Notification.FLAG_AUTO_CANCEL;
notificationManager.notify(NOTIFICATION_ID + decisionId, n);
}
/**
* Returns the top-most entry of the activity stack.
*
* @return the Context of the currently bound UI or the master context if none is bound
*/
Context getUI() {
return (foregroundAct != null) ? foregroundAct : master;
}
int interact(final String message, final int titleId) {
/* prepare the MTMDecision blocker object */
MTMDecision choice = new MTMDecision();
final int myId = createDecisionId(choice);
masterHandler.post(new Runnable() {
public void run() {
Intent ni = new Intent(master, MemorizingActivity.class);
ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
ni.putExtra(DECISION_INTENT_ID, myId);
ni.putExtra(DECISION_INTENT_CERT, message);
ni.putExtra(DECISION_TITLE_ID, titleId);
// we try to directly start the activity and fall back to
// making a notification
try {
getUI().startActivity(ni);
} catch (Exception e) {
LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
startActivityNotification(ni, myId, message);
}
}
});
LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId);
try {
synchronized(choice) { choice.wait(); }
} catch (InterruptedException e) {
LOGGER.log(Level.FINER, "InterruptedException", e);
}
LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state);
return choice.state;
}
void interactCert(final X509Certificate[] chain, String authType, CertificateException cause)
throws CertificateException
{
switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) {
case MTMDecision.DECISION_ALWAYS:
storeCert(chain[0]); // only store the server cert, not the whole chain
case MTMDecision.DECISION_ONCE:
break;
default:
throw (cause);
}
}
boolean interactHostname(X509Certificate cert, String hostname)
{
switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) {
case MTMDecision.DECISION_ALWAYS:
storeCert(hostname, cert);
case MTMDecision.DECISION_ONCE:
return true;
default:
return false;
}
}
protected static void interactResult(int decisionId, int choice) {
MTMDecision d;
synchronized(openDecisions) {
d = openDecisions.get(decisionId);
openDecisions.remove(decisionId);
}
if (d == null) {
LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!");
return;
}
synchronized(d) {
d.state = choice;
d.notify();
}
}
class MemorizingHostnameVerifier implements HostnameVerifier {
private HostnameVerifier defaultVerifier;
public MemorizingHostnameVerifier(HostnameVerifier wrapped) {
defaultVerifier = wrapped;
}
protected boolean verify(String hostname, SSLSession session, boolean interactive) {
LOGGER.log(Level.FINE, "hostname verifier for " + hostname + ", trying default verifier first");
// if the default verifier accepts the hostname, we are done
if (defaultVerifier.verify(hostname, session)) {
LOGGER.log(Level.FINE, "default verifier accepted " + hostname);
return true;
}
// otherwise, we check if the hostname is an alias for this cert in our keystore
try {
X509Certificate cert = (X509Certificate)session.getPeerCertificates()[0];
//Log.d(TAG, "cert: " + cert);
if (cert.equals(appKeyStore.getCertificate(hostname.toLowerCase(Locale.US)))) {
LOGGER.log(Level.FINE, "certificate for " + hostname + " is in our keystore. accepting.");
return true;
} else {
LOGGER.log(Level.FINE, "server " + hostname + " provided wrong certificate, asking user.");
if (interactive) {
return interactHostname(cert, hostname);
} else {
return false;
}
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean verify(String hostname, SSLSession session) {
return verify(hostname, session, true);
}
}
class NonInteractiveMemorizingHostnameVerifier extends MemorizingHostnameVerifier {
public NonInteractiveMemorizingHostnameVerifier(HostnameVerifier wrapped) {
super(wrapped);
}
@Override
public boolean verify(String hostname, SSLSession session) {
return verify(hostname, session, true);
}
}
public X509TrustManager getNonInteractive() {
return new NonInteractiveMemorizingTrustManager();
}
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return MemorizingTrustManager.this.getAcceptedIssuers();
}
}
}