Subtree merged in MemorizingTrustManager
This commit is contained in:
commit
41d2e72be7
|
@ -1 +0,0 @@
|
|||
Subproject commit fad835037adc1bd313bb56b694426fca4eb67346
|
|
@ -0,0 +1,11 @@
|
|||
bin
|
||||
build
|
||||
gen
|
||||
local.properties
|
||||
example/bin
|
||||
example/gen
|
||||
tags
|
||||
.project
|
||||
.classpath
|
||||
.gradle
|
||||
.*.swp
|
|
@ -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>
|
|
@ -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.
|
|
@ -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/)!
|
|
@ -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.
|
||||
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 *;
|
||||
#}
|
|
@ -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
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">MemorizingTrustManager Example</string>
|
||||
</resources>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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 *;
|
||||
#}
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 \"%s\" 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>
|
|
@ -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>
|
|
@ -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 n’est 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>
|
|
@ -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>
|
|
@ -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 \"%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">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>
|
|
@ -0,0 +1 @@
|
|||
include ':example'
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue