New Gradle project structure

This commit is contained in:
Dominik Schürmann
2014-01-27 14:18:25 +01:00
parent 08e57019b0
commit cb3ca37db9
359 changed files with 0 additions and 215 deletions

View File

@@ -0,0 +1,408 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.sufficientlysecure.keychain"
android:installLocation="auto"
android:versionCode="22000"
android:versionName="2.2" >
<!--
General remarks
===============
- Last APG 1 version was 10900 (1.0.9 beta 00)
- Keychain starting with versionCode 20000!
Association of file types to Keychain
=====================================
General remarks about file ending conventions:
- *.gpg for binary files
- *.asc for ascii armored files The actual content can be anything.
The file ending only shows if it is binary or ascii encoded.
Remarks about the ugly android:pathPattern:
- We are matching all files with a specific file ending.
This is done in an ugly way because of Android limitations.
Read http://stackoverflow.com/questions/1733195/android-intent-filter-for-a-particular-file-extension and http://stackoverflow.com/questions/3400072/pathpattern-to-match-file-extension-does-not-work-if-a-period-exists-elsewhere-i/8599921
for more information.
- Do _not_ set mimeType for gpg!
Cyanogenmod's file manager will only show Keychain for gpg files if no mimeType is set!
For OI Filemanager it makes no difference, gpg files can't be associated
-->
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="19" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.NFC" />
<!-- android:allowBackup="false": Don't allow backup over adb backup or other apps! -->
<application
android:name=".KeychainApplication"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:theme="@style/Theme.Sherlock.Light" >
<activity
android:name=".ui.KeyListPublicActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/app_name"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.SEARCH" /> -->
<!-- </intent-filter> -->
<!-- <meta-data -->
<!-- android:name="android.app.searchable" -->
<!-- android:resource="@xml/searchable_public_keys" /> -->
</activity>
<activity
android:name=".ui.KeyListSecretActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_manage_secret_keys"
android:launchMode="singleTop" >
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.SEARCH" /> -->
<!-- </intent-filter> -->
<!-- <meta-data -->
<!-- android:name="android.app.searchable" -->
<!-- android:resource="@xml/searchable_secret_keys" /> -->
</activity>
<activity
android:name=".ui.EditKeyActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_edit_key"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.ViewKeyActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_key_details"
android:parentActivityName=".ui.KeyListPublicActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.KeyListPublicActivity" />
</activity>
<activity
android:name=".ui.ViewKeyActivityJB"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_key_details"
android:parentActivityName=".ui.KeyListPublicActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.KeyListPublicActivity" />
</activity>
<activity
android:name=".ui.SelectPublicKeyActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_select_recipients"
android:launchMode="singleTop"
android:uiOptions="splitActionBarWhenNarrow" >
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.SEARCH" /> -->
<!-- </intent-filter> -->
<!-- <meta-data -->
<!-- android:name="android.app.searchable" -->
<!-- android:resource="@xml/searchable_public_keys" /> -->
</activity>
<activity
android:name=".ui.SelectSecretKeyActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_select_secret_key"
android:launchMode="singleTop" >
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.SEARCH" /> -->
<!-- </intent-filter> -->
<!-- <meta-data -->
<!-- android:name="android.app.searchable" -->
<!-- android:resource="@xml/searchable_secret_keys" /> -->
</activity>
<activity
android:name=".ui.EncryptActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_encrypt"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="stateHidden" >
<!-- Keychain's own Actions -->
<intent-filter>
<action android:name="org.sufficientlysecure.keychain.action.ENCRYPT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- Android's Send Action -->
<intent-filter android:label="@string/intent_send_encrypt" >
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.DecryptActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_decrypt"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="stateHidden" >
<!-- Keychain's own Actions -->
<intent-filter>
<action android:name="org.sufficientlysecure.keychain.action.DECRYPT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- Android's Send Action -->
<intent-filter android:label="@string/intent_send_decrypt" >
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- Linking "Decrypt" to file types -->
<intent-filter android:label="@string/intent_decrypt_file" >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="*" />
<data android:scheme="file" />
<data android:scheme="content" />
<!-- Workaround to match files in pathes with dots in them, like /cdcard/my.folder/test.gpg -->
<data android:pathPattern=".*\\.gpg" />
<data android:pathPattern=".*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
</intent-filter>
<intent-filter android:label="@string/intent_decrypt_file" >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="*" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.asc" />
<data android:pathPattern=".*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
</intent-filter>
</activity>
<activity
android:name=".ui.KeyServerQueryActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_key_server_query" />
<activity
android:name=".ui.KeyServerUploadActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_send_key" />
<activity
android:name=".ui.PreferencesActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_preferences" />
<activity
android:name=".ui.PreferencesKeyServerActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_key_server_preference"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.SignKeyActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_sign_key" />
<activity
android:name=".ui.ImportKeysActivity"
android:label="@string/title_import_keys"
android:launchMode="singleTop"
android:windowSoftInputMode="stateHidden" >
<!-- Handle NFC tags detected from outside our application -->
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<!-- mime type as defined in http://tools.ietf.org/html/rfc3156, section 7 -->
<data android:mimeType="application/pgp-keys" />
</intent-filter>
<!-- Keychain's own Actions -->
<intent-filter android:label="@string/intent_import_key" >
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- IMPORT again without mimeType to also allow data only without filename -->
<intent-filter android:label="@string/intent_import_key" >
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" />
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- Linking "Import key" to file types -->
<intent-filter android:label="@string/intent_import_key" >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="*" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:pathPattern=".*\\.gpg" />
<data android:pathPattern=".*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpg" />
</intent-filter>
<intent-filter android:label="@string/intent_import_key" >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="*" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.asc" />
<data android:pathPattern=".*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.asc" />
</intent-filter>
</activity>
<activity
android:name=".ui.HelpActivity"
android:label="@string/title_help" />
<!-- Internal services/content providers (not exported) -->
<service
android:name=".service.PassphraseCacheService"
android:exported="false"
android:process=":passphrase_cache" />
<service
android:name="org.sufficientlysecure.keychain.service.KeychainIntentService"
android:exported="false" />
<provider
android:name="org.sufficientlysecure.keychain.provider.KeychainProvider"
android:authorities="org.sufficientlysecure.keychain.provider"
android:exported="false" />
<!-- Internal classes of the remote APIs (not exported) -->
<activity
android:name="org.sufficientlysecure.keychain.service.remote.RemoteServiceActivity"
android:exported="false"
android:label="@string/app_name"
android:launchMode="singleTop"
android:process=":remote_api"
android:taskAffinity=":remote_api" />
<activity
android:name="org.sufficientlysecure.keychain.service.remote.RegisteredAppsListActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:exported="false"
android:label="@string/title_api_registered_apps" />
<activity
android:name="org.sufficientlysecure.keychain.service.remote.AppSettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:exported="false" />
<!-- OpenPGP Remote API -->
<service
android:name="org.sufficientlysecure.keychain.service.remote.OpenPgpService"
android:enabled="true"
android:exported="true"
android:process=":remote_api" >
<intent-filter>
<action android:name="org.openintents.openpgp.IOpenPgpService" />
</intent-filter>
<meta-data
android:name="api_version"
android:value="1" />
</service>
<!-- Extended Remote API -->
<service
android:name="org.sufficientlysecure.keychain.service.remote.ExtendedApiService"
android:enabled="true"
android:exported="true"
android:process=":remote_api" >
<intent-filter>
<action android:name="org.sufficientlysecure.keychain.service.remote.IExtendedApiService" />
</intent-filter>
<meta-data
android:name="api_version"
android:value="1" />
</service>
<!-- TODO: authority! Make this API with content provider uris -->
<!-- <provider -->
<!-- android:name="org.sufficientlysecure.keychain.provider.KeychainServiceBlobProvider" -->
<!-- android:authorities="org.sufficientlysecure.keychain.provider.KeychainServiceBlobProvider" -->
<!-- android:permission="org.sufficientlysecure.keychain.permission.ACCESS_API" /> -->
</application>
</manifest>

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.OpenPgpError;
interface IOpenPgpCallback {
/**
* onSuccess returns on successful OpenPGP operations.
*
* @param output
* contains resulting output (decrypted content (when input was encrypted)
* or content without signature (when input was signed-only))
* @param signatureResult
* signatureResult is only non-null if decryptAndVerify() was called and the content
* was encrypted or signed-and-encrypted.
*/
oneway void onSuccess(in OpenPgpData output, in OpenPgpSignatureResult signatureResult);
/**
* onError returns on errors or when allowUserInteraction was set to false, but user interaction
* was required execute an OpenPGP operation.
*
* @param error
* See OpenPgpError class for more information.
*/
oneway void onError(in OpenPgpError error);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpError;
interface IOpenPgpKeyIdsCallback {
/**
* onSuccess returns on successful getKeyIds operations.
*
* @param keyIds
* returned key ids
*/
oneway void onSuccess(in long[] keyIds);
/**
* onError returns on errors or when allowUserInteraction was set to false, but user interaction
* was required execute an OpenPGP operation.
*
* @param error
* See OpenPgpError class for more information.
*/
oneway void onError(in OpenPgpError error);
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.IOpenPgpCallback;
import org.openintents.openpgp.IOpenPgpKeyIdsCallback;
/**
* All methods are oneway, which means they are asynchronous and non-blocking.
* Results are returned to the callback, which has to be implemented on client side.
*/
interface IOpenPgpService {
/**
* Sign
*
* After successful signing, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param callback
* Callback where to return results
*/
oneway void sign(in OpenPgpData input, in OpenPgpData output, in IOpenPgpCallback callback);
/**
* Encrypt
*
* After successful encryption, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param keyIds
* Key Ids of recipients. Can be retrieved with getKeyIds()
* @param callback
* Callback where to return results
*/
oneway void encrypt(in OpenPgpData input, in OpenPgpData output, in long[] keyIds, in IOpenPgpCallback callback);
/**
* Sign then encrypt
*
* After successful signing and encryption, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param keyIds
* Key Ids of recipients. Can be retrieved with getKeyIds()
* @param callback
* Callback where to return results
*/
oneway void signAndEncrypt(in OpenPgpData input, in OpenPgpData output, in long[] keyIds, in IOpenPgpCallback callback);
/**
* Decrypts and verifies given input bytes. This methods handles encrypted-only, signed-and-encrypted,
* and also signed-only input.
*
* After successful decryption/verification, callback's onSuccess will contain the resulting output.
* The signatureResult in onSuccess is only non-null if signed-and-encrypted or signed-only inputBytes were given.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param callback
* Callback where to return results
*/
oneway void decryptAndVerify(in OpenPgpData input, in OpenPgpData output, in IOpenPgpCallback callback);
/**
* Get available key ids based on given user ids
*
* @param ids
* User Ids (emails) of recipients OR key ids
* @param allowUserInteraction
* Enable user interaction to lookup and import unknown keys
* @param callback
* Callback where to return results (different type than callback in other functions!)
*/
oneway void getKeyIds(in String[] ids, in boolean allowUserInteraction, in IOpenPgpKeyIdsCallback callback);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpData so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpData;

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpError so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpError;

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpSignatureResult so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpSignatureResult;

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.service.remote;
interface IExtendedApiCallback {
oneway void onSuccess(in byte[] outputBytes);
oneway void onError(in String error);
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.service.remote.IExtendedApiCallback;
/**
* All methods are oneway, which means they are asynchronous and non-blocking.
* Results are returned to the callback, which has to be implemented on client side.
*/
interface IExtendedApiService {
/**
* Symmetric Encrypt
*
* @param inputBytes
* Byte array you want to encrypt
* @param passphrase
* symmetric passhprase
* @param callback
* Callback where to return results
*/
oneway void encrypt(in byte[] inputBytes, in String passphrase, in IExtendedApiCallback callback);
/**
* Generates self signed X509 certificate signed by OpenPGP private key (from app settings)
*
* @param subjAltNameURI
* @param callback
* Callback where to return results
*/
oneway void selfSignedX509Cert(in String subjAltNameURI, in IExtendedApiCallback callback);
}

View File

@@ -0,0 +1,10 @@
package org.openintents.openpgp;
public class OpenPgpConstants {
public static final String TAG = "OpenPgp API";
public static final int REQUIRED_API_VERSION = 1;
public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService";
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import android.net.Uri;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
public class OpenPgpData implements Parcelable {
public static final int TYPE_STRING = 0;
public static final int TYPE_BYTE_ARRAY = 1;
public static final int TYPE_FILE_DESCRIPTOR = 2;
public static final int TYPE_URI = 3;
int type;
String string;
byte[] bytes = new byte[0];
ParcelFileDescriptor fileDescriptor;
Uri uri;
public int getType() {
return type;
}
public String getString() {
return string;
}
public byte[] getBytes() {
return bytes;
}
public ParcelFileDescriptor getFileDescriptor() {
return fileDescriptor;
}
public Uri getUri() {
return uri;
}
public OpenPgpData() {
}
/**
* Not a real constructor. This can be used to define requested output type.
*
* @param type
*/
public OpenPgpData(int type) {
this.type = type;
}
public OpenPgpData(String string) {
this.string = string;
this.type = TYPE_STRING;
}
public OpenPgpData(byte[] bytes) {
this.bytes = bytes;
this.type = TYPE_BYTE_ARRAY;
}
public OpenPgpData(ParcelFileDescriptor fileDescriptor) {
this.fileDescriptor = fileDescriptor;
this.type = TYPE_FILE_DESCRIPTOR;
}
public OpenPgpData(Uri uri) {
this.uri = uri;
this.type = TYPE_URI;
}
public OpenPgpData(OpenPgpData b) {
this.string = b.string;
this.bytes = b.bytes;
this.fileDescriptor = b.fileDescriptor;
this.uri = b.uri;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type);
dest.writeString(string);
dest.writeInt(bytes.length);
dest.writeByteArray(bytes);
dest.writeParcelable(fileDescriptor, 0);
dest.writeParcelable(uri, 0);
}
public static final Creator<OpenPgpData> CREATOR = new Creator<OpenPgpData>() {
public OpenPgpData createFromParcel(final Parcel source) {
OpenPgpData vr = new OpenPgpData();
vr.type = source.readInt();
vr.string = source.readString();
vr.bytes = new byte[source.readInt()];
source.readByteArray(vr.bytes);
vr.fileDescriptor = source.readParcelable(ParcelFileDescriptor.class.getClassLoader());
vr.fileDescriptor = source.readParcelable(Uri.class.getClassLoader());
return vr;
}
public OpenPgpData[] newArray(final int size) {
return new OpenPgpData[size];
}
};
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import android.os.Parcel;
import android.os.Parcelable;
public class OpenPgpError implements Parcelable {
public static final int GENERIC_ERROR = 0;
public static final int NO_OR_WRONG_PASSPHRASE = 1;
public static final int NO_USER_IDS = 2;
public static final int USER_INTERACTION_REQUIRED = 3;
int errorId;
String message;
public OpenPgpError() {
}
public OpenPgpError(int errorId, String message) {
this.errorId = errorId;
this.message = message;
}
public OpenPgpError(OpenPgpError b) {
this.errorId = b.errorId;
this.message = b.message;
}
public int getErrorId() {
return errorId;
}
public void setErrorId(int errorId) {
this.errorId = errorId;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(errorId);
dest.writeString(message);
}
public static final Creator<OpenPgpError> CREATOR = new Creator<OpenPgpError>() {
public OpenPgpError createFromParcel(final Parcel source) {
OpenPgpError error = new OpenPgpError();
error.errorId = source.readInt();
error.message = source.readString();
return error;
}
public OpenPgpError[] newArray(final int size) {
return new OpenPgpError[size];
}
};
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import java.util.List;
import java.util.regex.Pattern;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
public class OpenPgpHelper {
private Context context;
public static Pattern PGP_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
public static Pattern PGP_SIGNED_MESSAGE = Pattern
.compile(
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
Pattern.DOTALL);
public OpenPgpHelper(Context context) {
super();
this.context = context;
}
public boolean isAvailable() {
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import java.util.ArrayList;
import java.util.List;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.graphics.drawable.Drawable;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.TextView;
public class OpenPgpListPreference extends DialogPreference {
ArrayList<OpenPgpProviderEntry> mProviderList = new ArrayList<OpenPgpProviderEntry>();
private String mSelectedPackage;
public OpenPgpListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(
new Intent(OpenPgpConstants.SERVICE_INTENT), PackageManager.GET_META_DATA);
if (!resInfo.isEmpty()) {
for (ResolveInfo resolveInfo : resInfo) {
if (resolveInfo.serviceInfo == null)
continue;
String packageName = resolveInfo.serviceInfo.packageName;
String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(context
.getPackageManager()));
Drawable icon = resolveInfo.serviceInfo.loadIcon(context.getPackageManager());
// get api version
ServiceInfo si = resolveInfo.serviceInfo;
int apiVersion = si.metaData.getInt("api_version");
mProviderList.add(new OpenPgpProviderEntry(packageName, simpleName, icon,
apiVersion));
}
}
}
public OpenPgpListPreference(Context context) {
this(context, null);
}
/**
* Can be used to add "no selection"
*
* @param packageName
* @param simpleName
* @param icon
*/
public void addProvider(int position, String packageName, String simpleName, Drawable icon,
int apiVersion) {
mProviderList.add(position, new OpenPgpProviderEntry(packageName, simpleName, icon,
apiVersion));
}
@Override
protected void onPrepareDialogBuilder(Builder builder) {
// Init ArrayAdapter with OpenPGP Providers
ListAdapter adapter = new ArrayAdapter<OpenPgpProviderEntry>(getContext(),
android.R.layout.select_dialog_singlechoice, android.R.id.text1, mProviderList) {
public View getView(int position, View convertView, ViewGroup parent) {
// User super class to create the View
View v = super.getView(position, convertView, parent);
TextView tv = (TextView) v.findViewById(android.R.id.text1);
// Put the image on the TextView
tv.setCompoundDrawablesWithIntrinsicBounds(mProviderList.get(position).icon, null,
null, null);
// Add margin between image and text (support various screen densities)
int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f);
tv.setCompoundDrawablePadding(dp10);
// disable if it has the wrong api_version
if (mProviderList.get(position).apiVersion == OpenPgpConstants.REQUIRED_API_VERSION) {
tv.setEnabled(true);
} else {
tv.setEnabled(false);
tv.setText(tv.getText() + " (API v" + mProviderList.get(position).apiVersion
+ ", needs v" + OpenPgpConstants.REQUIRED_API_VERSION + ")");
}
return v;
}
};
builder.setSingleChoiceItems(adapter, getIndexOfProviderList(getValue()),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mSelectedPackage = mProviderList.get(which).packageName;
/*
* Clicking on an item simulates the positive button click, and dismisses
* the dialog.
*/
OpenPgpListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
}
});
/*
* The typical interaction for list-based dialogs is to have click-on-an-item dismiss the
* dialog instead of the user having to press 'Ok'.
*/
builder.setPositiveButton(null, null);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult && (mSelectedPackage != null)) {
if (callChangeListener(mSelectedPackage)) {
setValue(mSelectedPackage);
}
}
}
private int getIndexOfProviderList(String packageName) {
for (OpenPgpProviderEntry app : mProviderList) {
if (app.packageName.equals(packageName)) {
return mProviderList.indexOf(app);
}
}
return -1;
}
public void setValue(String packageName) {
mSelectedPackage = packageName;
persistString(packageName);
}
public String getValue() {
return mSelectedPackage;
}
public String getEntry() {
return getEntryByValue(mSelectedPackage);
}
public String getEntryByValue(String packageName) {
for (OpenPgpProviderEntry app : mProviderList) {
if (app.packageName.equals(packageName)) {
return app.simpleName;
}
}
return null;
}
private static class OpenPgpProviderEntry {
private String packageName;
private String simpleName;
private Drawable icon;
private int apiVersion;
public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon,
int apiVersion) {
this.packageName = packageName;
this.simpleName = simpleName;
this.icon = icon;
this.apiVersion = apiVersion;
}
@Override
public String toString() {
return simpleName;
}
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.IOpenPgpService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
public class OpenPgpServiceConnection {
private Context mApplicationContext;
private IOpenPgpService mService;
private boolean mBound;
private String mCryptoProviderPackageName;
public OpenPgpServiceConnection(Context context, String cryptoProviderPackageName) {
this.mApplicationContext = context.getApplicationContext();
this.mCryptoProviderPackageName = cryptoProviderPackageName;
}
public IOpenPgpService getService() {
return mService;
}
public boolean isBound() {
return mBound;
}
private ServiceConnection mCryptoServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IOpenPgpService.Stub.asInterface(service);
Log.d(OpenPgpConstants.TAG, "connected to service");
mBound = true;
}
public void onServiceDisconnected(ComponentName name) {
mService = null;
Log.d(OpenPgpConstants.TAG, "disconnected from service");
mBound = false;
}
};
/**
* If not already bound, bind!
*
* @return
*/
public boolean bindToService() {
if (mService == null && !mBound) { // if not already connected
try {
Log.d(OpenPgpConstants.TAG, "not bound yet");
Intent serviceIntent = new Intent();
serviceIntent.setAction(IOpenPgpService.class.getName());
serviceIntent.setPackage(mCryptoProviderPackageName);
mApplicationContext.bindService(serviceIntent, mCryptoServiceConnection,
Context.BIND_AUTO_CREATE);
return true;
} catch (Exception e) {
Log.d(OpenPgpConstants.TAG, "Exception on binding", e);
return false;
}
} else {
Log.d(OpenPgpConstants.TAG, "already bound");
return true;
}
}
public void unbindFromService() {
mApplicationContext.unbindService(mCryptoServiceConnection);
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import android.os.Parcel;
import android.os.Parcelable;
public class OpenPgpSignatureResult implements Parcelable {
// generic error on signature verification
public static final int SIGNATURE_ERROR = 0;
// successfully verified signature, with trusted public key
public static final int SIGNATURE_SUCCESS_TRUSTED = 1;
// no public key was found for this signature verification
// you can retrieve the key with
// getKeys(new String[] {String.valueOf(signatureResult.getKeyId)}, true, callback)
public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2;
// successfully verified signature, but with untrusted public key
public static final int SIGNATURE_SUCCESS_UNTRUSTED = 3;
int status;
boolean signatureOnly;
String userId;
long keyId;
public int getStatus() {
return status;
}
public boolean isSignatureOnly() {
return signatureOnly;
}
public String getUserId() {
return userId;
}
public long getKeyId() {
return keyId;
}
public OpenPgpSignatureResult() {
}
public OpenPgpSignatureResult(int signatureStatus, String signatureUserId,
boolean signatureOnly, long keyId) {
this.status = signatureStatus;
this.signatureOnly = signatureOnly;
this.userId = signatureUserId;
this.keyId = keyId;
}
public OpenPgpSignatureResult(OpenPgpSignatureResult b) {
this.status = b.status;
this.userId = b.userId;
this.signatureOnly = b.signatureOnly;
this.keyId = b.keyId;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(status);
dest.writeByte((byte) (signatureOnly ? 1 : 0));
dest.writeString(userId);
dest.writeLong(keyId);
}
public static final Creator<OpenPgpSignatureResult> CREATOR = new Creator<OpenPgpSignatureResult>() {
public OpenPgpSignatureResult createFromParcel(final Parcel source) {
OpenPgpSignatureResult vr = new OpenPgpSignatureResult();
vr.status = source.readInt();
vr.signatureOnly = source.readByte() == 1;
vr.userId = source.readString();
vr.keyId = source.readLong();
return vr;
}
public OpenPgpSignatureResult[] newArray(final int size) {
return new OpenPgpSignatureResult[size];
}
};
@Override
public String toString() {
String out = new String();
out += "\nstatus: " + status;
out += "\nuserId: " + userId;
out += "\nsignatureOnly: " + signatureOnly;
out += "\nkeyId: " + keyId;
return out;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import android.os.Environment;
public final class Constants {
public static final boolean DEBUG = BuildConfig.DEBUG;
public static final String TAG = "Keychain";
public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain";
// as defined in http://tools.ietf.org/html/rfc3156, section 7
public static final String NFC_MIME = "application/pgp-keys";
// Not BC due to the use of Spongy Castle for Android
public static final String SC = BouncyCastleProvider.PROVIDER_NAME;
public static final String BOUNCY_CASTLE_PROVIDER_NAME = SC;
public static final String INTENT_PREFIX = PACKAGE_NAME + ".action.";
public static final class path {
public static final String APP_DIR = Environment.getExternalStorageDirectory()
+ "/OpenPGP-Keychain";
}
public static final class pref {
public static final String DEFAULT_ENCRYPTION_ALGORITHM = "defaultEncryptionAlgorithm";
public static final String DEFAULT_HASH_ALGORITHM = "defaultHashAlgorithm";
public static final String DEFAULT_ASCII_ARMOUR = "defaultAsciiArmour";
public static final String DEFAULT_MESSAGE_COMPRESSION = "defaultMessageCompression";
public static final String DEFAULT_FILE_COMPRESSION = "defaultFileCompression";
public static final String PASS_PHRASE_CACHE_TTL = "passPhraseCacheTtl";
public static final String LANGUAGE = "language";
public static final String FORCE_V3_SIGNATURES = "forceV3Signatures";
public static final String KEY_SERVERS = "keyServers";
}
public static final class defaults {
public static final String KEY_SERVERS = "pool.sks-keyservers.net, subkeys.pgp.net, pgp.mit.edu";
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain;
import org.spongycastle.bcpg.CompressionAlgorithmTags;
/**
*
* TODO:
*
* - refactor ids, some are not needed and can be done with xml
*
*/
public final class Id {
public static final class menu {
public static final class option {
public static final int new_pass_phrase = 0x21070001;
public static final int create = 0x21070002;
public static final int about = 0x21070003;
public static final int manage_public_keys = 0x21070004;
public static final int manage_secret_keys = 0x21070005;
public static final int export_keys = 0x21070007;
public static final int preferences = 0x21070008;
public static final int search = 0x21070009;
public static final int help = 0x21070010;
public static final int key_server = 0x21070011;
public static final int scanQRCode = 0x21070012;
public static final int encrypt = 0x21070013;
public static final int encrypt_to_clipboard = 0x21070014;
public static final int decrypt = 0x21070015;
public static final int reply = 0x21070016;
public static final int cancel = 0x21070017;
public static final int save = 0x21070018;
public static final int okay = 0x21070019;
public static final int import_from_file = 0x21070020;
public static final int import_from_qr_code = 0x21070021;
public static final int import_from_nfc = 0x21070022;
public static final int crypto_consumers = 0x21070023;
public static final int createExpert = 0x21070024;
}
}
// use only lower 16 bits due to compatibility lib
public static final class message {
public static final int progress_update = 0x00006001;
public static final int done = 0x00006002;
public static final int import_keys = 0x00006003;
public static final int export_keys = 0x00006004;
public static final int import_done = 0x00006005;
public static final int export_done = 0x00006006;
public static final int create_key = 0x00006007;
public static final int edit_key = 0x00006008;
public static final int delete_done = 0x00006009;
public static final int query_done = 0x00006010;
public static final int unknown_signature_key = 0x00006011;
}
// use only lower 16 bits due to compatibility lib
public static final class request {
public static final int public_keys = 0x00007001;
public static final int secret_keys = 0x00007002;
public static final int filename = 0x00007003;
// public static final int output_filename = 0x00007004;
public static final int key_server_preference = 0x00007005;
public static final int look_up_key_id = 0x00007006;
public static final int export_to_server = 0x00007007;
public static final int import_from_qr_code = 0x00007008;
public static final int sign_key = 0x00007009;
}
public static final class dialog {
public static final int pass_phrase = 0x21070001;
public static final int encrypting = 0x21070002;
public static final int decrypting = 0x21070003;
public static final int new_pass_phrase = 0x21070004;
public static final int pass_phrases_do_not_match = 0x21070005;
public static final int no_pass_phrase = 0x21070006;
public static final int saving = 0x21070007;
public static final int delete_key = 0x21070008;
public static final int import_keys = 0x21070009;
public static final int importing = 0x2107000a;
public static final int export_key = 0x2107000b;
public static final int export_keys = 0x2107000c;
public static final int exporting = 0x2107000d;
public static final int new_account = 0x2107000e;
public static final int change_log = 0x21070010;
public static final int output_filename = 0x21070011;
public static final int delete_file = 0x21070012;
public static final int deleting = 0x21070013;
public static final int help = 0x21070014;
public static final int querying = 0x21070015;
public static final int lookup_unknown_key = 0x21070016;
public static final int signing = 0x21070017;
}
public static final class task {
public static final int import_keys = 0x21070001;
public static final int export_keys = 0x21070002;
}
public static final class type {
public static final int public_key = 0x21070001;
public static final int secret_key = 0x21070002;
public static final int user_id = 0x21070003;
public static final int key = 0x21070004;
}
public static final class choice {
public static final class algorithm {
public static final int dsa = 0x21070001;
public static final int elgamal = 0x21070002;
public static final int rsa = 0x21070003;
}
public static final class compression {
public static final int none = 0x21070001;
public static final int zlib = CompressionAlgorithmTags.ZLIB;
public static final int bzip2 = CompressionAlgorithmTags.BZIP2;
public static final int zip = CompressionAlgorithmTags.ZIP;
}
public static final class usage {
public static final int sign_only = 0x21070001;
public static final int encrypt_only = 0x21070002;
public static final int sign_and_encrypt = 0x21070003;
}
public static final class action {
public static final int encrypt = 0x21070001;
public static final int decrypt = 0x21070002;
public static final int import_public = 0x21070003;
public static final int import_secret = 0x21070004;
}
}
public static final class return_value {
public static final int ok = 0;
public static final int error = -1;
public static final int no_master_key = -2;
public static final int updated = 1;
public static final int bad = -3;
}
public static final class target {
public static final int clipboard = 0x21070001;
public static final int email = 0x21070002;
public static final int file = 0x21070003;
public static final int message = 0x21070004;
}
public static final class mode {
public static final int undefined = 0x21070001;
public static final int byte_array = 0x21070002;
public static final int file = 0x21070003;
public static final int stream = 0x21070004;
}
public static final class key {
public static final int none = 0;
public static final int symmetric = -1;
}
public static final class content {
public static final int unknown = 0;
public static final int encrypted_data = 1;
public static final int keys = 2;
}
public static final class keyserver {
public static final int search = 0x21070001;
public static final int get = 0x21070002;
public static final int add = 0x21070003;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain;
import java.io.File;
import java.security.Provider;
import java.security.Security;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.PRNGFixes;
import android.app.Application;
import android.os.Environment;
public class KeychainApplication extends Application {
/**
* Called when the application is starting, before any activity, service, or receiver objects
* (excluding content providers) have been created.
*/
@Override
public void onCreate() {
super.onCreate();
/*
* Sets Bouncy (Spongy) Castle as preferred security provider
*
* insertProviderAt() position starts from 1
*/
Security.insertProviderAt(new BouncyCastleProvider(), 1);
/*
* apply RNG fixes
*
* among other things, executes Security.insertProviderAt(new
* LinuxPRNGSecureRandomProvider(), 1) for Android <= SDK 17
*/
PRNGFixes.apply();
Log.d(Constants.TAG, "Bouncy Castle set and PRNG Fixes applied!");
if (Constants.DEBUG) {
Provider[] providers = Security.getProviders();
Log.d(Constants.TAG, "Installed Security Providers:");
for (Provider p : providers) {
Log.d(Constants.TAG, "provider class: " + p.getClass().getName());
}
}
// Create APG directory on sdcard if not existing
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File dir = new File(Constants.path.APP_DIR);
if (!dir.exists() && !dir.mkdirs()) {
// ignore this for now, it's not crucial
// that the directory doesn't exist at this point
}
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.compatibility;
import java.lang.reflect.Method;
import android.content.Context;
import org.sufficientlysecure.keychain.util.Log;
public class ClipboardReflection {
private static final String clipboardLabel = "Keychain";
/**
* Wrapper around ClipboardManager based on Android version using Reflection API
*
* @param context
* @param text
*/
public static void copyToClipboard(Context context, String text) {
Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
try {
if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
Method methodSetText = clipboard.getClass()
.getMethod("setText", CharSequence.class);
methodSetText.invoke(clipboard, text);
} else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
Class<?> classClipData = Class.forName("android.content.ClipData");
Method methodNewPlainText = classClipData.getMethod("newPlainText",
CharSequence.class, CharSequence.class);
Object clip = methodNewPlainText.invoke(null, clipboardLabel, text);
methodNewPlainText = clipboard.getClass()
.getMethod("setPrimaryClip", classClipData);
methodNewPlainText.invoke(clipboard, clip);
}
} catch (Exception e) {
Log.e("ProjectsException", "There was an error copying the text to the clipboard: "
+ e.getMessage());
}
}
/**
* Wrapper around ClipboardManager based on Android version using Reflection API
*
* @param context
* @param text
*/
public static CharSequence getClipboardText(Context context) {
Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
try {
if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
// CharSequence text = clipboard.getText();
Method methodGetText = clipboard.getClass().getMethod("getText");
Object text = methodGetText.invoke(clipboard);
return (CharSequence) text;
} else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
// ClipData clipData = clipboard.getPrimaryClip();
Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip");
Object clipData = methodGetPrimaryClip.invoke(clipboard);
// ClipData.Item clipDataItem = clipData.getItemAt(0);
Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class);
Object clipDataItem = methodGetItemAt.invoke(clipData, 0);
// CharSequence text = clipDataItem.coerceToText(context);
Method methodGetString = clipDataItem.getClass().getMethod("coerceToText",
Context.class);
Object text = methodGetString.invoke(clipDataItem, context);
return (CharSequence) text;
} else {
return null;
}
} catch (Exception e) {
Log.e("ProjectsException", "There was an error getting the text from the clipboard: "
+ e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.compatibility;
import android.os.Build;
import android.os.Handler;
/**
* Bug on Android >= 4.2
*
* http://code.google.com/p/android/issues/detail?id=41901
*
* DialogFragment disappears on pressing home and comming back. This also happens especially in
* FileDialogFragment after launching a file manager and coming back.
*
* Usage: <code>
* DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
* public void run() {
* // show dialog...
* }
* });
* </code>
*/
public class DialogFragmentWorkaround {
public static final SDKLevel17Interface INTERFACE = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) ? new SDKLevel17Impl()
: new SDKLevelPriorLevel17Impl());
private static final int RUNNABLE_DELAY = 300;
public interface SDKLevel17Interface {
// Workaround for http://code.google.com/p/android/issues/detail?id=41901
void runnableRunDelayed(Runnable runnable);
}
private static class SDKLevelPriorLevel17Impl implements SDKLevel17Interface {
@Override
public void runnableRunDelayed(Runnable runnable) {
runnable.run();
}
}
private static class SDKLevel17Impl implements SDKLevel17Interface {
@Override
public void runnableRunDelayed(Runnable runnable) {
new Handler().postDelayed(runnable, RUNNABLE_DELAY);
}
}
// Can't instantiate this class
private DialogFragmentWorkaround() {
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.compatibility;
import android.view.View;
import android.widget.ListView;
import com.actionbarsherlock.app.SherlockListFragment;
/**
* Bug on Android >= 4.1
*
* http://code.google.com/p/android/issues/detail?id=35885
*
* Items are not checked in layout
*/
public class ListFragmentWorkaround extends SherlockListFragment {
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
l.setItemChecked(position, l.isItemChecked(position));
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.helper;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.Log;
import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.TextView;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.SherlockFragmentActivity;
public class ActionBarHelper {
/**
* Set actionbar without home button if called from another app
*
* @param activity
*/
public static void setBackButton(SherlockFragmentActivity activity) {
// set actionbar without home button if called from another app
final ActionBar actionBar = activity.getSupportActionBar();
Log.d(Constants.TAG, "calling package (only set when using startActivityForResult)="
+ activity.getCallingPackage());
if (activity.getCallingPackage() != null
&& activity.getCallingPackage().equals(Constants.PACKAGE_NAME)) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
} else {
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setHomeButtonEnabled(false);
}
}
/**
* Sets custom view on ActionBar for Done/Cancel activities
*
* @param actionBar
* @param doneText
* @param doneOnClickListener
* @param cancelText
* @param cancelOnClickListener
*/
public static void setDoneCancelView(ActionBar actionBar, int doneText,
OnClickListener doneOnClickListener, int cancelText,
OnClickListener cancelOnClickListener) {
// Inflate a "Done"/"Cancel" custom action bar view
final LayoutInflater inflater = (LayoutInflater) actionBar.getThemedContext()
.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
final View customActionBarView = inflater.inflate(
R.layout.actionbar_custom_view_done_cancel, null);
((TextView) customActionBarView.findViewById(R.id.actionbar_done_text)).setText(doneText);
customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener(
doneOnClickListener);
((TextView) customActionBarView.findViewById(R.id.actionbar_cancel_text))
.setText(cancelText);
customActionBarView.findViewById(R.id.actionbar_cancel).setOnClickListener(
cancelOnClickListener);
// Show the custom action bar view and hide the normal Home icon and title.
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setCustomView(customActionBarView, new ActionBar.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
/**
* Sets custom view on ActionBar for Done activities
*
* @param actionBar
* @param doneText
* @param doneOnClickListener
*/
public static void setDoneView(ActionBar actionBar, int doneText,
OnClickListener doneOnClickListener) {
// Inflate a "Done" custom action bar view to serve as the "Up" affordance.
final LayoutInflater inflater = (LayoutInflater) actionBar.getThemedContext()
.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
final View customActionBarView = inflater
.inflate(R.layout.actionbar_custom_view_done, null);
((TextView) customActionBarView.findViewById(R.id.actionbar_done_text)).setText(doneText);
customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener(
doneOnClickListener);
// Show the custom action bar view and hide the normal Home icon and title.
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setCustomView(customActionBarView);
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.helper;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
public class ExportHelper {
protected FileDialogFragment mFileDialog;
protected String mExportFilename;
SherlockFragmentActivity activity;
public ExportHelper(SherlockFragmentActivity activity) {
super();
this.activity = activity;
}
public void deleteKey(Uri dataUri, final int keyType, Handler deleteHandler) {
long keyRingRowId = Long.valueOf(dataUri.getLastPathSegment());
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(deleteHandler);
DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger,
new long[] { keyRingRowId }, keyType);
deleteKeyDialog.show(activity.getSupportFragmentManager(), "deleteKeyDialog");
}
/**
* Show dialog where to export keys
*
* @param keyRingMasterKeyId
* if -1 export all keys
*/
public void showExportKeysDialog(final Uri dataUri, final int keyType,
final String exportFilename) {
mExportFilename = exportFilename;
// Message is received after file is selected
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == FileDialogFragment.MESSAGE_OKAY) {
Bundle data = message.getData();
mExportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
exportKeys(dataUri, keyType);
}
}
};
// Create a new Messenger for the communication back
final Messenger messenger = new Messenger(returnHandler);
DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
public void run() {
String title = null;
if (dataUri == null) {
// export all keys
title = activity.getString(R.string.title_export_keys);
} else {
// export only key specified at data uri
title = activity.getString(R.string.title_export_key);
}
String message = null;
if (keyType == Id.type.public_key) {
message = activity.getString(R.string.specify_file_to_export_to);
} else {
message = activity.getString(R.string.specify_file_to_export_secret_keys_to);
}
mFileDialog = FileDialogFragment.newInstance(messenger, title, message,
exportFilename, null);
mFileDialog.show(activity.getSupportFragmentManager(), "fileDialog");
}
});
}
/**
* Export keys
*
* @param keyRingMasterKeyId
* if -1 export all keys
*/
public void exportKeys(Uri dataUri, int keyType) {
Log.d(Constants.TAG, "exportKeys started");
// Send all information needed to service to export key in other thread
Intent intent = new Intent(activity, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_EXPORT_KEYRING);
// fill values for this action
Bundle data = new Bundle();
data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFilename);
data.putInt(KeychainIntentService.EXPORT_KEY_TYPE, keyType);
if (dataUri == null) {
data.putBoolean(KeychainIntentService.EXPORT_ALL, true);
} else {
// TODO: put data uri into service???
long keyRingMasterKeyId = ProviderHelper.getMasterKeyId(activity, dataUri);
data.putLong(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, keyRingMasterKeyId);
}
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after exporting is done in ApgService
KeychainIntentServiceHandler exportHandler = new KeychainIntentServiceHandler(activity,
R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
// get returned data bundle
Bundle returnData = message.getData();
int exported = returnData.getInt(KeychainIntentService.RESULT_EXPORT);
String toastMessage;
if (exported == 1) {
toastMessage = activity.getString(R.string.key_exported);
} else if (exported > 0) {
toastMessage = activity.getString(R.string.keys_exported, exported);
} else {
toastMessage = activity.getString(R.string.no_keys_exported);
}
Toast.makeText(activity, toastMessage, Toast.LENGTH_SHORT).show();
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(exportHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
exportHandler.showProgressDialog(activity);
// start service with intent
activity.startService(intent);
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.helper;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.Log;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.support.v4.app.Fragment;
import android.widget.Toast;
public class FileHelper {
/**
* Checks if external storage is mounted if file is located on external storage
*
* @param file
* @return true if storage is mounted
*/
public static boolean isStorageMounted(String file) {
if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return false;
}
}
return true;
}
/**
* Opens the preferred installed file manager on Android and shows a toast if no manager is
* installed.
*
* @param activity
* @param filename
* default selected file, not supported by all file managers
* @param mimeType
* can be text/plain for example
* @param requestCode
* requestCode used to identify the result coming back from file manager to
* onActivityResult() in your activity
*/
public static void openFile(Activity activity, String filename, String mimeType, int requestCode) {
Intent intent = buildFileIntent(filename, mimeType);
try {
activity.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
// No compatible file manager was found.
Toast.makeText(activity, R.string.no_filemanager_installed, Toast.LENGTH_SHORT).show();
}
}
public static void openFile(Fragment fragment, String filename, String mimeType, int requestCode) {
Intent intent = buildFileIntent(filename, mimeType);
try {
fragment.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
// No compatible file manager was found.
Toast.makeText(fragment.getActivity(), R.string.no_filemanager_installed,
Toast.LENGTH_SHORT).show();
}
}
private static Intent buildFileIntent(String filename, String mimeType) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setData(Uri.parse("file://" + filename));
intent.setType(mimeType);
return intent;
}
/**
* Get a file path from a Uri.
*
* from https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/
* afilechooser/utils/FileUtils.java
*
* @param context
* @param uri
* @return
*
* @author paulburke
*/
public static String getPath(Context context, Uri uri) {
Log.d(Constants.TAG + " File -",
"Authority: " + uri.getAuthority() + ", Fragment: " + uri.getFragment()
+ ", Port: " + uri.getPort() + ", Query: " + uri.getQuery() + ", Scheme: "
+ uri.getScheme() + ", Host: " + uri.getHost() + ", Segments: "
+ uri.getPathSegments().toString());
if ("content".equalsIgnoreCase(uri.getScheme())) {
String[] projection = { "_data" };
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, projection, null, null, null);
int column_index = cursor.getColumnIndexOrThrow("_data");
if (cursor.moveToFirst()) {
return cursor.getString(column_index);
}
} catch (Exception e) {
// Eat it
}
}
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.helper;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.Set;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
import android.os.Bundle;
public class OtherHelper {
/**
* Return the number if days between two dates
*
* @param first
* @param second
* @return number of days
*/
public static long getNumDaysBetween(GregorianCalendar first, GregorianCalendar second) {
GregorianCalendar tmp = new GregorianCalendar();
tmp.setTime(first.getTime());
long numDays = (second.getTimeInMillis() - first.getTimeInMillis()) / 1000 / 86400;
tmp.add(Calendar.DAY_OF_MONTH, (int) numDays);
while (tmp.before(second)) {
tmp.add(Calendar.DAY_OF_MONTH, 1);
++numDays;
}
return numDays;
}
/**
* Logs bundle content to debug for inspecting the content
*
* @param bundle
* @param bundleName
*/
public static void logDebugBundle(Bundle bundle, String bundleName) {
if (Constants.DEBUG) {
if (bundle != null) {
Set<String> ks = bundle.keySet();
Iterator<String> iterator = ks.iterator();
Log.d(Constants.TAG, "Bundle " + bundleName + ":");
Log.d(Constants.TAG, "------------------------------");
while (iterator.hasNext()) {
String key = iterator.next();
Object value = bundle.get(key);
if (value != null) {
Log.d(Constants.TAG, key + " : " + value.toString());
} else {
Log.d(Constants.TAG, key + " : null");
}
}
Log.d(Constants.TAG, "------------------------------");
} else {
Log.d(Constants.TAG, "Bundle " + bundleName + ": null");
}
}
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.helper;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.openpgp.PGPEncryptedData;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.Vector;
/**
* Singleton Implementation of a Preference Helper
*/
public class Preferences {
private static Preferences mPreferences;
private SharedPreferences mSharedPreferences;
public static synchronized Preferences getPreferences(Context context) {
return getPreferences(context, false);
}
public static synchronized Preferences getPreferences(Context context, boolean force_new) {
if (mPreferences == null || force_new) {
mPreferences = new Preferences(context);
}
return mPreferences;
}
private Preferences(Context context) {
mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_PRIVATE);
}
public String getLanguage() {
return mSharedPreferences.getString(Constants.pref.LANGUAGE, "");
}
public void setLanguage(String value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(Constants.pref.LANGUAGE, value);
editor.commit();
}
public long getPassPhraseCacheTtl() {
int ttl = mSharedPreferences.getInt(Constants.pref.PASS_PHRASE_CACHE_TTL, 180);
// fix the value if it was set to "never" in previous versions, which currently is not
// supported
if (ttl == 0) {
ttl = 180;
}
return (long) ttl;
}
public void setPassPhraseCacheTtl(int value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putInt(Constants.pref.PASS_PHRASE_CACHE_TTL, value);
editor.commit();
}
public int getDefaultEncryptionAlgorithm() {
return mSharedPreferences.getInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM,
PGPEncryptedData.AES_256);
}
public void setDefaultEncryptionAlgorithm(int value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM, value);
editor.commit();
}
public int getDefaultHashAlgorithm() {
return mSharedPreferences.getInt(Constants.pref.DEFAULT_HASH_ALGORITHM,
HashAlgorithmTags.SHA512);
}
public void setDefaultHashAlgorithm(int value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putInt(Constants.pref.DEFAULT_HASH_ALGORITHM, value);
editor.commit();
}
public int getDefaultMessageCompression() {
return mSharedPreferences.getInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION,
Id.choice.compression.zlib);
}
public void setDefaultMessageCompression(int value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION, value);
editor.commit();
}
public int getDefaultFileCompression() {
return mSharedPreferences.getInt(Constants.pref.DEFAULT_FILE_COMPRESSION,
Id.choice.compression.none);
}
public void setDefaultFileCompression(int value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putInt(Constants.pref.DEFAULT_FILE_COMPRESSION, value);
editor.commit();
}
public boolean getDefaultAsciiArmour() {
return mSharedPreferences.getBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, false);
}
public void setDefaultAsciiArmour(boolean value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, value);
editor.commit();
}
public boolean getForceV3Signatures() {
return mSharedPreferences.getBoolean(Constants.pref.FORCE_V3_SIGNATURES, false);
}
public void setForceV3Signatures(boolean value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(Constants.pref.FORCE_V3_SIGNATURES, value);
editor.commit();
}
public String[] getKeyServers() {
String rawData = mSharedPreferences.getString(Constants.pref.KEY_SERVERS,
Constants.defaults.KEY_SERVERS);
Vector<String> servers = new Vector<String>();
String chunks[] = rawData.split(",");
for (int i = 0; i < chunks.length; ++i) {
String tmp = chunks[i].trim();
if (tmp.length() > 0) {
servers.add(tmp);
}
}
return servers.toArray(chunks);
}
public void setKeyServers(String[] value) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
String rawData = "";
for (int i = 0; i < value.length; ++i) {
String tmp = value[i].trim();
if (tmp.length() == 0) {
continue;
}
if (!"".equals(rawData)) {
rawData += ",";
}
rawData += tmp;
}
editor.putString(Constants.pref.KEY_SERVERS, rawData);
editor.commit();
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.pgp;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPObjectFactory;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
public class PgpConversionHelper {
/**
* Convert from byte[] to PGPKeyRing
*
* @param keysBytes
* @return
*/
public static PGPKeyRing BytesToPGPKeyRing(byte[] keysBytes) {
PGPObjectFactory factory = new PGPObjectFactory(keysBytes);
PGPKeyRing keyRing = null;
try {
if ((keyRing = (PGPKeyRing) factory.nextObject()) == null) {
Log.e(Constants.TAG, "No keys given!");
}
} catch (IOException e) {
Log.e(Constants.TAG, "Error while converting to PGPKeyRing!", e);
}
return keyRing;
}
/**
* Convert from byte[] to ArrayList<PGPSecretKey>
*
* @param keysBytes
* @return
*/
public static ArrayList<PGPSecretKey> BytesToPGPSecretKeyList(byte[] keysBytes) {
PGPSecretKeyRing keyRing = (PGPSecretKeyRing) BytesToPGPKeyRing(keysBytes);
ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
@SuppressWarnings("unchecked")
Iterator<PGPSecretKey> itr = keyRing.getSecretKeys();
while (itr.hasNext()) {
keys.add(itr.next());
}
return keys;
}
/**
* Convert from byte[] to PGPSecretKey
*
* Singles keys are encoded as keyRings with one single key in it by Bouncy Castle
*
* @param keysBytes
* @return
*/
public static PGPSecretKey BytesToPGPSecretKey(byte[] keyBytes) {
PGPSecretKey key = BytesToPGPSecretKeyList(keyBytes).get(0);
return key;
}
/**
* Convert from ArrayList<PGPSecretKey> to byte[]
*
* @param keys
* @return
*/
public static byte[] PGPSecretKeyArrayListToBytes(ArrayList<PGPSecretKey> keys) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
for (PGPSecretKey key : keys) {
try {
key.encode(os);
} catch (IOException e) {
Log.e(Constants.TAG, "Error while converting ArrayList<PGPSecretKey> to byte[]!", e);
}
}
return os.toByteArray();
}
/**
* Convert from PGPSecretKey to byte[]
*
* @param keysBytes
* @return
*/
public static byte[] PGPSecretKeyToBytes(PGPSecretKey key) {
try {
return key.getEncoded();
} catch (IOException e) {
Log.e(Constants.TAG, "Encoding failed", e);
return null;
}
}
/**
* Convert from PGPSecretKeyRing to byte[]
*
* @param keysBytes
* @return
*/
public static byte[] PGPSecretKeyRingToBytes(PGPSecretKeyRing keyRing) {
try {
return keyRing.getEncoded();
} catch (IOException e) {
Log.e(Constants.TAG, "Encoding failed", e);
return null;
}
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.pgp;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.security.SecureRandom;
import java.util.Iterator;
import java.util.regex.Pattern;
import org.spongycastle.openpgp.PGPEncryptedDataList;
import org.spongycastle.openpgp.PGPObjectFactory;
import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.PGPUtil;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
public class PgpHelper {
public static Pattern PGP_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
public static Pattern PGP_SIGNED_MESSAGE = Pattern
.compile(
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
Pattern.DOTALL);
public static Pattern PGP_PUBLIC_KEY = Pattern.compile(
".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*",
Pattern.DOTALL);
public static String getVersion(Context context) {
String version = null;
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(Constants.PACKAGE_NAME, 0);
version = pi.versionName;
return version;
} catch (NameNotFoundException e) {
Log.e(Constants.TAG, "Version could not be retrieved!", e);
return "0.0.0";
}
}
public static String getFullVersion(Context context) {
return "OpenPGP Keychain v" + getVersion(context);
}
public static long getDecryptionKeyId(Context context, InputStream inputStream)
throws PgpGeneralException, NoAsymmetricEncryptionException, IOException {
InputStream in = PGPUtil.getDecoderStream(inputStream);
PGPObjectFactory pgpF = new PGPObjectFactory(in);
PGPEncryptedDataList enc;
Object o = pgpF.nextObject();
// the first object might be a PGP marker packet.
if (o instanceof PGPEncryptedDataList) {
enc = (PGPEncryptedDataList) o;
} else {
enc = (PGPEncryptedDataList) pgpF.nextObject();
}
if (enc == null) {
throw new PgpGeneralException(context.getString(R.string.error_invalid_data));
}
// TODO: currently we always only look at the first known key
// find the secret key
PGPSecretKey secretKey = null;
Iterator<?> it = enc.getEncryptedDataObjects();
boolean gotAsymmetricEncryption = false;
while (it.hasNext()) {
Object obj = it.next();
if (obj instanceof PGPPublicKeyEncryptedData) {
gotAsymmetricEncryption = true;
PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) obj;
secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, pbe.getKeyID());
if (secretKey != null) {
break;
}
}
}
if (!gotAsymmetricEncryption) {
throw new NoAsymmetricEncryptionException();
}
if (secretKey == null) {
return Id.key.none;
}
return secretKey.getKeyID();
}
public static int getStreamContent(Context context, InputStream inStream) throws IOException {
InputStream in = PGPUtil.getDecoderStream(inStream);
PGPObjectFactory pgpF = new PGPObjectFactory(in);
Object object = pgpF.nextObject();
while (object != null) {
if (object instanceof PGPPublicKeyRing || object instanceof PGPSecretKeyRing) {
return Id.content.keys;
} else if (object instanceof PGPEncryptedDataList) {
return Id.content.encrypted_data;
}
object = pgpF.nextObject();
}
return Id.content.unknown;
}
/**
* Generate a random filename
*
* @param length
* @return
*/
public static String generateRandomFilename(int length) {
SecureRandom random = new SecureRandom();
byte bytes[] = new byte[length];
random.nextBytes(bytes);
String result = "";
for (int i = 0; i < length; ++i) {
int v = (bytes[i] + 256) % 64;
if (v < 10) {
result += (char) ('0' + v);
} else if (v < 36) {
result += (char) ('A' + v - 10);
} else if (v < 62) {
result += (char) ('a' + v - 36);
} else if (v == 62) {
result += '_';
} else if (v == 63) {
result += '.';
}
}
return result;
}
/**
* Go once through stream to get length of stream. The length is later used to display progress
* when encrypting/decrypting
*
* @param in
* @return
* @throws IOException
*/
public static long getLengthOfStream(InputStream in) throws IOException {
long size = 0;
long n = 0;
byte dummy[] = new byte[0x10000];
while ((n = in.read(dummy)) > 0) {
size += n;
}
return size;
}
/**
* Deletes file securely by overwriting it with random data before deleting it.
*
* TODO: Does this really help on flash storage?
*
* @param context
* @param progress
* @param file
* @throws FileNotFoundException
* @throws IOException
*/
public static void deleteFileSecurely(Context context, ProgressDialogUpdater progress, File file)
throws FileNotFoundException, IOException {
long length = file.length();
SecureRandom random = new SecureRandom();
RandomAccessFile raf = new RandomAccessFile(file, "rws");
raf.seek(0);
raf.getFilePointer();
byte[] data = new byte[1 << 16];
int pos = 0;
String msg = context.getString(R.string.progress_deleting_securely, file.getName());
while (pos < length) {
if (progress != null)
progress.setProgress(msg, (int) (100 * pos / length), 100);
random.nextBytes(data);
raf.write(data);
pos += data.length;
}
raf.close();
file.delete();
}
}

View File

@@ -0,0 +1,314 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.pgp;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import org.spongycastle.bcpg.ArmoredOutputStream;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPObjectFactory;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.PGPUtil;
import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.util.HkpKeyServer;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.KeyServer.AddKeyException;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
public class PgpImportExport {
private Context mContext;
private ProgressDialogUpdater mProgress;
public PgpImportExport(Context context, ProgressDialogUpdater progress) {
super();
this.mContext = context;
this.mProgress = progress;
}
public void updateProgress(int message, int current, int total) {
if (mProgress != null) {
mProgress.setProgress(message, current, total);
}
}
public void updateProgress(String message, int current, int total) {
if (mProgress != null) {
mProgress.setProgress(message, current, total);
}
}
public void updateProgress(int current, int total) {
if (mProgress != null) {
mProgress.setProgress(current, total);
}
}
public boolean uploadKeyRingToServer(HkpKeyServer server, PGPPublicKeyRing keyring) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ArmoredOutputStream aos = new ArmoredOutputStream(bos);
try {
aos.write(keyring.getEncoded());
aos.close();
String armouredKey = bos.toString("UTF-8");
server.add(armouredKey);
return true;
} catch (IOException e) {
return false;
} catch (AddKeyException e) {
// TODO: tell the user?
return false;
} finally {
try {
bos.close();
} catch (IOException e) {
}
}
}
/**
* Imports keys from given data. If keyIds is given only those are imported
*
* @param data
* @param keyIds
* @return
* @throws PgpGeneralException
* @throws FileNotFoundException
* @throws PGPException
* @throws IOException
*/
public Bundle importKeyRings(InputData data, ArrayList<Long> keyIds)
throws PgpGeneralException, FileNotFoundException, PGPException, IOException {
Bundle returnData = new Bundle();
updateProgress(R.string.progress_importing_secret_keys, 0, 100);
PositionAwareInputStream progressIn = new PositionAwareInputStream(data.getInputStream());
// need to have access to the bufferedInput, so we can reuse it for the possible
// PGPObject chunks after the first one, e.g. files with several consecutive ASCII
// armour blocks
BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
int newKeys = 0;
int oldKeys = 0;
int badKeys = 0;
try {
// read all available blocks... (asc files can contain many blocks with BEGIN END)
while (bufferedInput.available() > 0) {
InputStream in = PGPUtil.getDecoderStream(bufferedInput);
PGPObjectFactory objectFactory = new PGPObjectFactory(in);
// go through all objects in this block
Object obj;
while ((obj = objectFactory.nextObject()) != null) {
Log.d(Constants.TAG, "Found class: " + obj.getClass());
if (obj instanceof PGPKeyRing) {
PGPKeyRing keyring = (PGPKeyRing) obj;
int status = Integer.MIN_VALUE; // out of bounds value
if (keyIds != null) {
if (keyIds.contains(keyring.getPublicKey().getKeyID())) {
status = storeKeyRingInCache(keyring);
} else {
Log.d(Constants.TAG, "not selected! key id: "
+ keyring.getPublicKey().getKeyID());
}
} else {
status = storeKeyRingInCache(keyring);
}
if (status == Id.return_value.error) {
throw new PgpGeneralException(
mContext.getString(R.string.error_saving_keys));
}
// update the counts to display to the user at the end
if (status == Id.return_value.updated) {
++oldKeys;
} else if (status == Id.return_value.ok) {
++newKeys;
} else if (status == Id.return_value.bad) {
++badKeys;
}
updateProgress((int) (100 * progressIn.position() / data.getSize()), 100);
} else {
Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
}
}
}
} catch (Exception e) {
Log.e(Constants.TAG, "Exception on parsing key file!", e);
}
returnData.putInt(KeychainIntentService.RESULT_IMPORT_ADDED, newKeys);
returnData.putInt(KeychainIntentService.RESULT_IMPORT_UPDATED, oldKeys);
returnData.putInt(KeychainIntentService.RESULT_IMPORT_BAD, badKeys);
updateProgress(R.string.progress_done, 100, 100);
return returnData;
}
public Bundle exportKeyRings(ArrayList<Long> keyRingMasterKeyIds, int keyType,
OutputStream outStream) throws PgpGeneralException, FileNotFoundException,
PGPException, IOException {
Bundle returnData = new Bundle();
updateProgress(
mContext.getResources().getQuantityString(R.plurals.progress_exporting_key,
keyRingMasterKeyIds.size()), 0, 100);
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
throw new PgpGeneralException(
mContext.getString(R.string.error_external_storage_not_ready));
}
// export public keyrings...
ArmoredOutputStream outPub = new ArmoredOutputStream(outStream);
outPub.setHeader("Version", PgpHelper.getFullVersion(mContext));
int numKeys = 0;
for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) {
// double the needed time if exporting both public and secret parts
if (keyType == Id.type.secret_key) {
updateProgress(i * 100 / keyRingMasterKeyIds.size() / 2, 100);
} else {
updateProgress(i * 100 / keyRingMasterKeyIds.size(), 100);
}
PGPPublicKeyRing publicKeyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(
mContext, keyRingMasterKeyIds.get(i));
if (publicKeyRing != null) {
publicKeyRing.encode(outPub);
}
++numKeys;
}
outPub.close();
// if we export secret keyrings, append all secret parts after the public parts
if (keyType == Id.type.secret_key) {
ArmoredOutputStream outSec = new ArmoredOutputStream(outStream);
outSec.setHeader("Version", PgpHelper.getFullVersion(mContext));
for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) {
updateProgress(i * 100 / keyRingMasterKeyIds.size() / 2, 100);
PGPSecretKeyRing secretKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
mContext, keyRingMasterKeyIds.get(i));
if (secretKeyRing != null) {
secretKeyRing.encode(outSec);
}
}
outSec.close();
}
returnData.putInt(KeychainIntentService.RESULT_EXPORT, numKeys);
updateProgress(R.string.progress_done, 100, 100);
return returnData;
}
/**
* TODO: implement Id.return_value.updated as status when key already existed
*
* @param context
* @param keyring
* @return
*/
@SuppressWarnings("unchecked")
public int storeKeyRingInCache(PGPKeyRing keyring) {
int status = Integer.MIN_VALUE; // out of bounds value (Id.return_value.*)
try {
if (keyring instanceof PGPSecretKeyRing) {
PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyring;
boolean save = true;
for (PGPSecretKey testSecretKey : new IterableIterator<PGPSecretKey>(
secretKeyRing.getSecretKeys())) {
if (!testSecretKey.isMasterKey()) {
if (PgpKeyHelper.isSecretKeyPrivateEmpty(testSecretKey)) {
// this is bad, something is very wrong...
save = false;
status = Id.return_value.bad;
}
}
}
if (save) {
ProviderHelper.saveKeyRing(mContext, secretKeyRing);
// TODO: preserve certifications
// (http://osdir.com/ml/encryption.bouncy-castle.devel/2007-01/msg00054.html ?)
PGPPublicKeyRing newPubRing = null;
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(
secretKeyRing.getPublicKeys())) {
if (newPubRing == null) {
newPubRing = new PGPPublicKeyRing(key.getEncoded(),
new JcaKeyFingerprintCalculator());
}
newPubRing = PGPPublicKeyRing.insertPublicKey(newPubRing, key);
}
if (newPubRing != null)
ProviderHelper.saveKeyRing(mContext, newPubRing);
// TODO: remove status returns, use exceptions!
status = Id.return_value.ok;
}
} else if (keyring instanceof PGPPublicKeyRing) {
PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
ProviderHelper.saveKeyRing(mContext, publicKeyRing);
// TODO: remove status returns, use exceptions!
status = Id.return_value.ok;
}
} catch (IOException e) {
status = Id.return_value.error;
}
return status;
}
}

View File

@@ -0,0 +1,563 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.pgp;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.PGPSignature;
import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Context;
public class PgpKeyHelper {
/**
* Returns the last 9 chars of a fingerprint
*
* @param fingerprint
* String containing short or long fingerprint
* @return
*/
public static String shortifyFingerprint(String fingerprint) {
return fingerprint.substring(41);
}
public static Date getCreationDate(PGPPublicKey key) {
return key.getCreationTime();
}
public static Date getCreationDate(PGPSecretKey key) {
return key.getPublicKey().getCreationTime();
}
@SuppressWarnings("unchecked")
public static PGPPublicKey getMasterKey(PGPPublicKeyRing keyRing) {
if (keyRing == null) {
return null;
}
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
if (key.isMasterKey()) {
return key;
}
}
return null;
}
@SuppressWarnings("unchecked")
public static PGPSecretKey getMasterKey(PGPSecretKeyRing keyRing) {
if (keyRing == null) {
return null;
}
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
if (key.isMasterKey()) {
return key;
}
}
return null;
}
@SuppressWarnings("unchecked")
public static PGPSecretKey getKeyNum(PGPSecretKeyRing keyRing, long num) {
long cnt = 0;
if (keyRing == null) {
return null;
}
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
if (cnt == num) {
return key;
}
cnt++;
}
return null;
}
@SuppressWarnings("unchecked")
public static Vector<PGPPublicKey> getEncryptKeys(PGPPublicKeyRing keyRing) {
Vector<PGPPublicKey> encryptKeys = new Vector<PGPPublicKey>();
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
if (isEncryptionKey(key)) {
encryptKeys.add(key);
}
}
return encryptKeys;
}
@SuppressWarnings("unchecked")
public static Vector<PGPSecretKey> getSigningKeys(PGPSecretKeyRing keyRing) {
Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
if (isSigningKey(key)) {
signingKeys.add(key);
}
}
return signingKeys;
}
@SuppressWarnings("unchecked")
public static Vector<PGPSecretKey> getCertificationKeys(PGPSecretKeyRing keyRing) {
Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
if (isCertificationKey(key)) {
signingKeys.add(key);
}
}
return signingKeys;
}
public static Vector<PGPPublicKey> getUsableEncryptKeys(PGPPublicKeyRing keyRing) {
Vector<PGPPublicKey> usableKeys = new Vector<PGPPublicKey>();
Vector<PGPPublicKey> encryptKeys = getEncryptKeys(keyRing);
PGPPublicKey masterKey = null;
for (int i = 0; i < encryptKeys.size(); ++i) {
PGPPublicKey key = encryptKeys.get(i);
if (!isExpired(key) && !key.isRevoked()) {
if (key.isMasterKey()) {
masterKey = key;
} else {
usableKeys.add(key);
}
}
}
if (masterKey != null) {
usableKeys.add(masterKey);
}
return usableKeys;
}
public static boolean isExpired(PGPPublicKey key) {
Date creationDate = getCreationDate(key);
Date expiryDate = getExpiryDate(key);
Date now = new Date();
if (now.compareTo(creationDate) >= 0
&& (expiryDate == null || now.compareTo(expiryDate) <= 0)) {
return false;
}
return true;
}
public static boolean isExpired(PGPSecretKey key) {
return isExpired(key.getPublicKey());
}
public static Vector<PGPSecretKey> getUsableCertificationKeys(PGPSecretKeyRing keyRing) {
Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
Vector<PGPSecretKey> signingKeys = getCertificationKeys(keyRing);
PGPSecretKey masterKey = null;
for (int i = 0; i < signingKeys.size(); ++i) {
PGPSecretKey key = signingKeys.get(i);
if (key.isMasterKey()) {
masterKey = key;
} else {
usableKeys.add(key);
}
}
if (masterKey != null) {
usableKeys.add(masterKey);
}
return usableKeys;
}
public static Vector<PGPSecretKey> getUsableSigningKeys(PGPSecretKeyRing keyRing) {
Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
Vector<PGPSecretKey> signingKeys = getSigningKeys(keyRing);
PGPSecretKey masterKey = null;
for (int i = 0; i < signingKeys.size(); ++i) {
PGPSecretKey key = signingKeys.get(i);
if (key.isMasterKey()) {
masterKey = key;
} else {
usableKeys.add(key);
}
}
if (masterKey != null) {
usableKeys.add(masterKey);
}
return usableKeys;
}
public static Date getExpiryDate(PGPPublicKey key) {
Date creationDate = getCreationDate(key);
if (key.getValidDays() == 0) {
// no expiry
return null;
}
Calendar calendar = GregorianCalendar.getInstance();
calendar.setTime(creationDate);
calendar.add(Calendar.DATE, key.getValidDays());
Date expiryDate = calendar.getTime();
return expiryDate;
}
public static Date getExpiryDate(PGPSecretKey key) {
return getExpiryDate(key.getPublicKey());
}
public static PGPPublicKey getEncryptPublicKey(Context context, long masterKeyId) {
PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(context,
masterKeyId);
if (keyRing == null) {
Log.e(Constants.TAG, "keyRing is null!");
return null;
}
Vector<PGPPublicKey> encryptKeys = getUsableEncryptKeys(keyRing);
if (encryptKeys.size() == 0) {
Log.e(Constants.TAG, "encryptKeys is null!");
return null;
}
return encryptKeys.get(0);
}
public static PGPSecretKey getCertificationKey(Context context, long masterKeyId) {
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(context,
masterKeyId);
if (keyRing == null) {
return null;
}
Vector<PGPSecretKey> signingKeys = getUsableCertificationKeys(keyRing);
if (signingKeys.size() == 0) {
return null;
}
return signingKeys.get(0);
}
public static PGPSecretKey getSigningKey(Context context, long masterKeyId) {
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(context,
masterKeyId);
if (keyRing == null) {
return null;
}
Vector<PGPSecretKey> signingKeys = getUsableSigningKeys(keyRing);
if (signingKeys.size() == 0) {
return null;
}
return signingKeys.get(0);
}
@SuppressWarnings("unchecked")
public static String getMainUserId(PGPPublicKey key) {
for (String userId : new IterableIterator<String>(key.getUserIDs())) {
return userId;
}
return null;
}
@SuppressWarnings("unchecked")
public static String getMainUserId(PGPSecretKey key) {
for (String userId : new IterableIterator<String>(key.getUserIDs())) {
return userId;
}
return null;
}
public static String getMainUserIdSafe(Context context, PGPPublicKey key) {
String userId = getMainUserId(key);
if (userId == null || userId.equals("")) {
userId = context.getString(R.string.unknown_user_id);
}
return userId;
}
public static String getMainUserIdSafe(Context context, PGPSecretKey key) {
String userId = getMainUserId(key);
if (userId == null || userId.equals("")) {
userId = context.getString(R.string.unknown_user_id);
}
return userId;
}
@SuppressWarnings("unchecked")
public static boolean isEncryptionKey(PGPPublicKey key) {
if (!key.isEncryptionKey()) {
return false;
}
if (key.getVersion() <= 3) {
// this must be true now
return key.isEncryptionKey();
}
// special cases
if (key.getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT) {
return true;
}
if (key.getAlgorithm() == PGPPublicKey.RSA_ENCRYPT) {
return true;
}
for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
continue;
}
PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
if (hashed != null
&& (hashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
return true;
}
PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
if (unhashed != null
&& (unhashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
return true;
}
}
return false;
}
public static boolean isEncryptionKey(PGPSecretKey key) {
return isEncryptionKey(key.getPublicKey());
}
@SuppressWarnings("unchecked")
public static boolean isSigningKey(PGPPublicKey key) {
if (key.getVersion() <= 3) {
return true;
}
// special case
if (key.getAlgorithm() == PGPPublicKey.RSA_SIGN) {
return true;
}
for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
continue;
}
PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
if (hashed != null && (hashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
return true;
}
PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
return true;
}
}
return false;
}
public static boolean isSigningKey(PGPSecretKey key) {
return isSigningKey(key.getPublicKey());
}
@SuppressWarnings("unchecked")
public static boolean isCertificationKey(PGPPublicKey key) {
if (key.getVersion() <= 3) {
return true;
}
for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
continue;
}
PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
if (hashed != null && (hashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) {
return true;
}
PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) {
return true;
}
}
return false;
}
public static boolean isCertificationKey(PGPSecretKey key) {
return isCertificationKey(key.getPublicKey());
}
public static String getAlgorithmInfo(PGPPublicKey key) {
return getAlgorithmInfo(key.getAlgorithm(), key.getBitStrength());
}
public static String getAlgorithmInfo(PGPSecretKey key) {
return getAlgorithmInfo(key.getPublicKey());
}
public static String getAlgorithmInfo(int algorithm, int keySize) {
String algorithmStr = null;
switch (algorithm) {
case PGPPublicKey.RSA_ENCRYPT:
case PGPPublicKey.RSA_GENERAL:
case PGPPublicKey.RSA_SIGN: {
algorithmStr = "RSA";
break;
}
case PGPPublicKey.DSA: {
algorithmStr = "DSA";
break;
}
case PGPPublicKey.ELGAMAL_ENCRYPT:
case PGPPublicKey.ELGAMAL_GENERAL: {
algorithmStr = "ElGamal";
break;
}
default: {
algorithmStr = "Unknown";
break;
}
}
return algorithmStr + ", " + keySize + " bit";
}
/**
* Converts fingerprint to hex with whitespaces after 4 characters
*
* @param fp
* @return
*/
public static String convertFingerprintToHex(byte[] fp) {
String fingerPrint = "";
for (int i = 0; i < fp.length; ++i) {
if (i != 0 && i % 10 == 0) {
fingerPrint += " ";
} else if (i != 0 && i % 2 == 0) {
fingerPrint += " ";
}
String chunk = Integer.toHexString((fp[i] + 256) % 256).toUpperCase(Locale.US);
while (chunk.length() < 2) {
chunk = "0" + chunk;
}
fingerPrint += chunk;
}
return fingerPrint;
}
public static String getFingerPrint(Context context, long keyId) {
PGPPublicKey key = ProviderHelper.getPGPPublicKeyByKeyId(context, keyId);
// if it is no public key get it from your own keys...
if (key == null) {
PGPSecretKey secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, keyId);
if (secretKey == null) {
Log.e(Constants.TAG, "Key could not be found!");
return null;
}
key = secretKey.getPublicKey();
}
return convertFingerprintToHex(key.getFingerprint());
}
public static boolean isSecretKeyPrivateEmpty(PGPSecretKey secretKey) {
return secretKey.isPrivateKeyEmpty();
}
public static boolean isSecretKeyPrivateEmpty(Context context, long keyId) {
PGPSecretKey secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, keyId);
if (secretKey == null) {
Log.e(Constants.TAG, "Key could not be found!");
return false; // could be a public key, assume it is not empty
}
return isSecretKeyPrivateEmpty(secretKey);
}
public static String convertKeyIdToHex(long keyId) {
String fingerPrint = Long.toHexString(keyId & 0xffffffffL).toUpperCase(Locale.US);
while (fingerPrint.length() < 8) {
fingerPrint = "0" + fingerPrint;
}
return fingerPrint;
}
/**
* TODO: documentation
*
* @param keyId
* @return
*/
public static String convertKeyToHex(long keyId) {
return convertKeyIdToHex(keyId >> 32) + convertKeyIdToHex(keyId);
}
public static long convertHexToKeyId(String data) {
int len = data.length();
String s2 = data.substring(len - 8);
String s1 = data.substring(0, len - 8);
return (Long.parseLong(s1, 16) << 32) | Long.parseLong(s2, 16);
}
/**
* Splits userId string into naming part, email part, and comment part
*
* @param userId
* @return array with naming (0), email (1), comment (2)
*/
public static String[] splitUserId(String userId) {
String[] result = new String[] { "", "", "" };
Pattern withComment = Pattern.compile("^(.*) \\((.*)\\) <(.*)>$");
Matcher matcher = withComment.matcher(userId);
if (matcher.matches()) {
result[0] = matcher.group(1);
result[1] = matcher.group(3);
result[2] = matcher.group(2);
return result;
}
Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$");
matcher = withoutComment.matcher(userId);
if (matcher.matches()) {
result[0] = matcher.group(1);
result[1] = matcher.group(2);
return result;
}
return result;
}
}

View File

@@ -0,0 +1,466 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.pgp;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Date;
import org.spongycastle.bcpg.CompressionAlgorithmTags;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.jce.spec.ElGamalParameterSpec;
import org.spongycastle.openpgp.PGPEncryptedData;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPKeyPair;
import org.spongycastle.openpgp.PGPKeyRingGenerator;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.PGPSignature;
import org.spongycastle.openpgp.PGPSignatureGenerator;
import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
import org.spongycastle.openpgp.PGPUtil;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor;
import org.spongycastle.openpgp.operator.PGPContentSignerBuilder;
import org.spongycastle.openpgp.operator.PGPDigestCalculator;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Primes;
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
import android.content.Context;
public class PgpKeyOperation {
private Context mContext;
private ProgressDialogUpdater mProgress;
private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[] {
SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192,
SymmetricKeyAlgorithmTags.AES_128, SymmetricKeyAlgorithmTags.CAST5,
SymmetricKeyAlgorithmTags.TRIPLE_DES };
private static final int[] PREFERRED_HASH_ALGORITHMS = new int[] { HashAlgorithmTags.SHA1,
HashAlgorithmTags.SHA256, HashAlgorithmTags.RIPEMD160 };
private static final int[] PREFERRED_COMPRESSION_ALGORITHMS = new int[] {
CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2,
CompressionAlgorithmTags.ZIP };
public PgpKeyOperation(Context context, ProgressDialogUpdater progress) {
super();
this.mContext = context;
this.mProgress = progress;
}
public void updateProgress(int message, int current, int total) {
if (mProgress != null) {
mProgress.setProgress(message, current, total);
}
}
public void updateProgress(int current, int total) {
if (mProgress != null) {
mProgress.setProgress(current, total);
}
}
/**
* Creates new secret key. The returned PGPSecretKeyRing contains only one newly generated key
* when this key is the new masterkey. If a masterkey is supplied in the parameters
* PGPSecretKeyRing contains the masterkey and the new key as a subkey (certified by the
* masterkey).
*
* @param algorithmChoice
* @param keySize
* @param passPhrase
* @param masterSecretKey
* @return
* @throws NoSuchAlgorithmException
* @throws PGPException
* @throws NoSuchProviderException
* @throws PgpGeneralException
* @throws InvalidAlgorithmParameterException
*/
public PGPSecretKeyRing createKey(int algorithmChoice, int keySize, String passPhrase,
PGPSecretKey masterSecretKey) throws NoSuchAlgorithmException, PGPException,
NoSuchProviderException, PgpGeneralException, InvalidAlgorithmParameterException {
if (keySize < 512) {
throw new PgpGeneralException(mContext.getString(R.string.error_key_size_minimum512bit));
}
if (passPhrase == null) {
passPhrase = "";
}
int algorithm = 0;
KeyPairGenerator keyGen = null;
switch (algorithmChoice) {
case Id.choice.algorithm.dsa: {
keyGen = KeyPairGenerator.getInstance("DSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
keyGen.initialize(keySize, new SecureRandom());
algorithm = PGPPublicKey.DSA;
break;
}
case Id.choice.algorithm.elgamal: {
if (masterSecretKey == null) {
throw new PgpGeneralException(
mContext.getString(R.string.error_master_key_must_not_be_el_gamal));
}
keyGen = KeyPairGenerator.getInstance("ElGamal", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
BigInteger p = Primes.getBestPrime(keySize);
BigInteger g = new BigInteger("2");
ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, g);
keyGen.initialize(elParams);
algorithm = PGPPublicKey.ELGAMAL_ENCRYPT;
break;
}
case Id.choice.algorithm.rsa: {
keyGen = KeyPairGenerator.getInstance("RSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
keyGen.initialize(keySize, new SecureRandom());
algorithm = PGPPublicKey.RSA_GENERAL;
break;
}
default: {
throw new PgpGeneralException(
mContext.getString(R.string.error_unknown_algorithm_choice));
}
}
// build new key pair
PGPKeyPair keyPair = new JcaPGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date());
// define hashing and signing algos
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
HashAlgorithmTags.SHA1);
// Build key encrypter and decrypter based on passphrase
PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
PGPEncryptedData.CAST5, sha1Calc)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passPhrase.toCharArray());
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passPhrase.toCharArray());
PGPKeyRingGenerator ringGen = null;
PGPContentSignerBuilder certificationSignerBuilder = null;
if (masterSecretKey == null) {
certificationSignerBuilder = new JcaPGPContentSignerBuilder(keyPair.getPublicKey()
.getAlgorithm(), HashAlgorithmTags.SHA1);
// build keyRing with only this one master key in it!
ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, keyPair, "",
sha1Calc, null, null, certificationSignerBuilder, keyEncryptor);
} else {
PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
PGPPrivateKey masterPrivateKey = masterSecretKey.extractPrivateKey(keyDecryptor);
PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
certificationSignerBuilder = new JcaPGPContentSignerBuilder(masterKeyPair
.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
// build keyRing with master key and new key as subkey (certified by masterkey)
ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, masterKeyPair,
"", sha1Calc, null, null, certificationSignerBuilder, keyEncryptor);
ringGen.addSubKey(keyPair);
}
PGPSecretKeyRing secKeyRing = ringGen.generateSecretKeyRing();
return secKeyRing;
}
public void changeSecretKeyPassphrase(PGPSecretKeyRing keyRing, String oldPassPhrase,
String newPassPhrase) throws IOException, PGPException, PGPException,
NoSuchProviderException {
updateProgress(R.string.progress_building_key, 0, 100);
if (oldPassPhrase == null) {
oldPassPhrase = "";
}
if (newPassPhrase == null) {
newPassPhrase = "";
}
PGPSecretKeyRing newKeyRing = PGPSecretKeyRing.copyWithNewPassword(
keyRing,
new JcePBESecretKeyDecryptorBuilder(new JcaPGPDigestCalculatorProviderBuilder()
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build()).setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray()),
new JcePBESecretKeyEncryptorBuilder(keyRing.getSecretKey()
.getKeyEncryptionAlgorithm()).build(newPassPhrase.toCharArray()));
updateProgress(R.string.progress_saving_key_ring, 50, 100);
ProviderHelper.saveKeyRing(mContext, newKeyRing);
updateProgress(R.string.progress_done, 100, 100);
}
public void buildSecretKey(ArrayList<String> userIds, ArrayList<PGPSecretKey> keys,
ArrayList<Integer> keysUsages, long masterKeyId, String oldPassPhrase,
String newPassPhrase) throws PgpGeneralException, NoSuchProviderException,
PGPException, NoSuchAlgorithmException, SignatureException, IOException {
Log.d(Constants.TAG, "userIds: " + userIds.toString());
updateProgress(R.string.progress_building_key, 0, 100);
if (oldPassPhrase == null) {
oldPassPhrase = "";
}
if (newPassPhrase == null) {
newPassPhrase = "";
}
updateProgress(R.string.progress_preparing_master_key, 10, 100);
int usageId = keysUsages.get(0);
boolean canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
boolean canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
String mainUserId = userIds.get(0);
PGPSecretKey masterKey = keys.get(0);
// this removes all userIds and certifications previously attached to the masterPublicKey
PGPPublicKey tmpKey = masterKey.getPublicKey();
PGPPublicKey masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(),
tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime());
// already done by code above:
// PGPPublicKey masterPublicKey = masterKey.getPublicKey();
// // Somehow, the PGPPublicKey already has an empty certification attached to it when the
// // keyRing is generated the first time, we remove that when it exists, before adding the
// new
// // ones
// PGPPublicKey masterPublicKeyRmCert = PGPPublicKey.removeCertification(masterPublicKey,
// "");
// if (masterPublicKeyRmCert != null) {
// masterPublicKey = masterPublicKeyRmCert;
// }
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray());
PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
updateProgress(R.string.progress_certifying_master_key, 20, 100);
// TODO: if we are editing a key, keep old certs, don't remake certs we don't have to.
for (String userId : userIds) {
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification);
}
PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
int keyFlags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA;
if (canEncrypt) {
keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
}
hashedPacketsGen.setKeyFlags(true, keyFlags);
hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
// TODO: this doesn't work quite right yet (APG 1)
// if (keyEditor.getExpiryDate() != null) {
// GregorianCalendar creationDate = new GregorianCalendar();
// creationDate.setTime(getCreationDate(masterKey));
// GregorianCalendar expiryDate = keyEditor.getExpiryDate();
// long numDays = Utils.getNumDaysBetween(creationDate, expiryDate);
// if (numDays <= 0) {
// throw new GeneralException(
// context.getString(R.string.error_expiryMustComeAfterCreation));
// }
// hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
// }
updateProgress(R.string.progress_building_master_key, 30, 100);
// define hashing and signing algos
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
HashAlgorithmTags.SHA1);
PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder(
masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
// Build key encrypter based on passphrase
PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
PGPEncryptedData.CAST5, sha1Calc)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
newPassPhrase.toCharArray());
PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(),
unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor);
updateProgress(R.string.progress_adding_sub_keys, 40, 100);
for (int i = 1; i < keys.size(); ++i) {
updateProgress(40 + 50 * (i - 1) / (keys.size() - 1), 100);
PGPSecretKey subKey = keys.get(i);
PGPPublicKey subPublicKey = subKey.getPublicKey();
PBESecretKeyDecryptor keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder()
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
oldPassPhrase.toCharArray());
PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2);
// TODO: now used without algorithm and creation time?! (APG 1)
PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey);
hashedPacketsGen = new PGPSignatureSubpacketGenerator();
unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
keyFlags = 0;
usageId = keysUsages.get(i);
canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
if (canSign) { // TODO: ensure signing times are the same, like gpg
keyFlags |= KeyFlags.SIGN_DATA;
// cross-certify signing keys
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
subPublicKey.getAlgorithm(), PGPUtil.SHA1)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
sGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey);
PGPSignature certification = sGen.generateCertification(masterPublicKey,
subPublicKey);
unhashedPacketsGen.setEmbeddedSignature(false, certification);
}
if (canEncrypt) {
keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
}
hashedPacketsGen.setKeyFlags(false, keyFlags);
// TODO: this doesn't work quite right yet (APG 1)
// if (keyEditor.getExpiryDate() != null) {
// GregorianCalendar creationDate = new GregorianCalendar();
// creationDate.setTime(getCreationDate(masterKey));
// GregorianCalendar expiryDate = keyEditor.getExpiryDate();
// long numDays = Utils.getNumDaysBetween(creationDate, expiryDate);
// if (numDays <= 0) {
// throw new GeneralException(
// context.getString(R.string.error_expiryMustComeAfterCreation));
// }
// hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
// }
keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate());
}
PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
updateProgress(R.string.progress_saving_key_ring, 90, 100);
ProviderHelper.saveKeyRing(mContext, secretKeyRing);
ProviderHelper.saveKeyRing(mContext, publicKeyRing);
updateProgress(R.string.progress_done, 100, 100);
}
public PGPPublicKeyRing signKey(long masterKeyId, long pubKeyId, String passphrase)
throws PgpGeneralException, NoSuchAlgorithmException, NoSuchProviderException,
PGPException, SignatureException {
if (passphrase == null) {
throw new PgpGeneralException("Unable to obtain passphrase");
} else {
PGPPublicKeyRing pubring = ProviderHelper
.getPGPPublicKeyRingByKeyId(mContext, pubKeyId);
PGPSecretKey signingKey = PgpKeyHelper.getCertificationKey(mContext, masterKeyId);
if (signingKey == null) {
throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed));
}
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
PGPPrivateKey signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
if (signaturePrivateKey == null) {
throw new PgpGeneralException(
mContext.getString(R.string.error_could_not_extract_private_key));
}
// TODO: SHA256 fixed?
JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
contentSignerBuilder);
signatureGenerator.init(PGPSignature.DIRECT_KEY, signaturePrivateKey);
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
PGPSignatureSubpacketVector packetVector = spGen.generate();
signatureGenerator.setHashedSubpackets(packetVector);
PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId),
signatureGenerator.generate());
pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
return pubring;
}
}
}

View File

@@ -0,0 +1,307 @@
package org.sufficientlysecure.keychain.pgp;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Vector;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.spongycastle.asn1.DERObjectIdentifier;
import org.spongycastle.asn1.x509.AuthorityKeyIdentifier;
import org.spongycastle.asn1.x509.BasicConstraints;
import org.spongycastle.asn1.x509.GeneralName;
import org.spongycastle.asn1.x509.GeneralNames;
import org.spongycastle.asn1.x509.SubjectKeyIdentifier;
import org.spongycastle.asn1.x509.X509Extensions;
import org.spongycastle.asn1.x509.X509Name;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.x509.X509V3CertificateGenerator;
import org.spongycastle.x509.extension.AuthorityKeyIdentifierStructure;
import org.spongycastle.x509.extension.SubjectKeyIdentifierStructure;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
public class PgpToX509 {
public final static String DN_COMMON_PART_O = "OpenPGP to X.509 Bridge";
public final static String DN_COMMON_PART_OU = "OpenPGP Keychain cert";
/**
* Creates a self-signed certificate from a public and private key. The (critical) key-usage
* extension is set up with: digital signature, non-repudiation, key-encipherment, key-agreement
* and certificate-signing. The (non-critical) Netscape extension is set up with: SSL client and
* S/MIME. A URI subjectAltName may also be set up.
*
* @param pubKey
* public key
* @param privKey
* private key
* @param subject
* subject (and issuer) DN for this certificate, RFC 2253 format preferred.
* @param startDate
* date from which the certificate will be valid (defaults to current date and time
* if null)
* @param endDate
* date until which the certificate will be valid (defaults to current date and time
* if null) *
* @param subjAltNameURI
* URI to be placed in subjectAltName
* @return self-signed certificate
* @throws InvalidKeyException
* @throws SignatureException
* @throws NoSuchAlgorithmException
* @throws IllegalStateException
* @throws NoSuchProviderException
* @throws CertificateException
* @throws Exception
*
* @author Bruno Harbulot
*/
public static X509Certificate createSelfSignedCert(PublicKey pubKey, PrivateKey privKey,
X509Name subject, Date startDate, Date endDate, String subjAltNameURI)
throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException,
SignatureException, CertificateException, NoSuchProviderException {
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
certGenerator.reset();
/*
* Sets up the subject distinguished name. Since it's a self-signed certificate, issuer and
* subject are the same.
*/
certGenerator.setIssuerDN(subject);
certGenerator.setSubjectDN(subject);
/*
* Sets up the validity dates.
*/
if (startDate == null) {
startDate = new Date(System.currentTimeMillis());
}
certGenerator.setNotBefore(startDate);
if (endDate == null) {
endDate = new Date(startDate.getTime() + (365L * 24L * 60L * 60L * 1000L));
Log.d(Constants.TAG, "end date is=" + DateFormat.getDateInstance().format(endDate));
}
certGenerator.setNotAfter(endDate);
/*
* The serial-number of this certificate is 1. It makes sense because it's self-signed.
*/
certGenerator.setSerialNumber(BigInteger.ONE);
/*
* Sets the public-key to embed in this certificate.
*/
certGenerator.setPublicKey(pubKey);
/*
* Sets the signature algorithm.
*/
String pubKeyAlgorithm = pubKey.getAlgorithm();
if (pubKeyAlgorithm.equals("DSA")) {
certGenerator.setSignatureAlgorithm("SHA1WithDSA");
} else if (pubKeyAlgorithm.equals("RSA")) {
certGenerator.setSignatureAlgorithm("SHA1WithRSAEncryption");
} else {
RuntimeException re = new RuntimeException("Algorithm not recognised: "
+ pubKeyAlgorithm);
Log.e(Constants.TAG, re.getMessage(), re);
throw re;
}
/*
* Adds the Basic Constraint (CA: true) extension.
*/
certGenerator.addExtension(X509Extensions.BasicConstraints, true,
new BasicConstraints(true));
/*
* Adds the subject key identifier extension.
*/
SubjectKeyIdentifier subjectKeyIdentifier = new SubjectKeyIdentifierStructure(pubKey);
certGenerator
.addExtension(X509Extensions.SubjectKeyIdentifier, false, subjectKeyIdentifier);
/*
* Adds the authority key identifier extension.
*/
AuthorityKeyIdentifier authorityKeyIdentifier = new AuthorityKeyIdentifierStructure(pubKey);
certGenerator.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
authorityKeyIdentifier);
/*
* Adds the subject alternative-name extension.
*/
if (subjAltNameURI != null) {
GeneralNames subjectAltNames = new GeneralNames(new GeneralName(
GeneralName.uniformResourceIdentifier, subjAltNameURI));
certGenerator.addExtension(X509Extensions.SubjectAlternativeName, false,
subjectAltNames);
}
/*
* Creates and sign this certificate with the private key corresponding to the public key of
* the certificate (hence the name "self-signed certificate").
*/
X509Certificate cert = certGenerator.generate(privKey);
/*
* Checks that this certificate has indeed been correctly signed.
*/
cert.verify(pubKey);
return cert;
}
/**
* Creates a self-signed certificate from a PGP Secret Key.
*
* @param pgpSecKey
* PGP Secret Key (from which one can extract the public and private keys and other
* attributes).
* @param pgpPrivKey
* PGP Private Key corresponding to the Secret Key (password callbacks should be done
* before calling this method)
* @param subjAltNameURI
* optional URI to embed in the subject alternative-name
* @return self-signed certificate
* @throws PGPException
* @throws NoSuchProviderException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws SignatureException
* @throws CertificateException
*
* @author Bruno Harbulot
*/
public static X509Certificate createSelfSignedCert(PGPSecretKey pgpSecKey,
PGPPrivateKey pgpPrivKey, String subjAltNameURI) throws PGPException,
NoSuchProviderException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException, CertificateException {
// get public key from secret key
PGPPublicKey pgpPubKey = pgpSecKey.getPublicKey();
// LOGGER.info("Key ID: " + Long.toHexString(pgpPubKey.getKeyID() & 0xffffffffL));
/*
* The X.509 Name to be the subject DN is prepared. The CN is extracted from the Secret Key
* user ID.
*/
Vector<DERObjectIdentifier> x509NameOids = new Vector<DERObjectIdentifier>();
Vector<String> x509NameValues = new Vector<String>();
x509NameOids.add(X509Name.O);
x509NameValues.add(DN_COMMON_PART_O);
x509NameOids.add(X509Name.OU);
x509NameValues.add(DN_COMMON_PART_OU);
for (@SuppressWarnings("unchecked")
Iterator<Object> it = (Iterator<Object>) pgpSecKey.getUserIDs(); it.hasNext();) {
Object attrib = it.next();
x509NameOids.add(X509Name.CN);
x509NameValues.add("CryptoCall");
// x509NameValues.add(attrib.toString());
}
/*
* Currently unused.
*/
Log.d(Constants.TAG, "User attributes: ");
for (@SuppressWarnings("unchecked")
Iterator<Object> it = (Iterator<Object>) pgpSecKey.getUserAttributes(); it.hasNext();) {
Object attrib = it.next();
Log.d(Constants.TAG, " - " + attrib + " -- " + attrib.getClass());
}
X509Name x509name = new X509Name(x509NameOids, x509NameValues);
Log.d(Constants.TAG, "Subject DN: " + x509name);
/*
* To check the signature from the certificate on the recipient side, the creation time
* needs to be embedded in the certificate. It seems natural to make this creation time be
* the "not-before" date of the X.509 certificate. Unlimited PGP keys have a validity of 0
* second. In this case, the "not-after" date will be the same as the not-before date. This
* is something that needs to be checked by the service receiving this certificate.
*/
Date creationTime = pgpPubKey.getCreationTime();
Log.d(Constants.TAG,
"pgp pub key creation time=" + DateFormat.getDateInstance().format(creationTime));
Log.d(Constants.TAG, "pgp valid seconds=" + pgpPubKey.getValidSeconds());
Date validTo = null;
if (pgpPubKey.getValidSeconds() > 0) {
validTo = new Date(creationTime.getTime() + 1000L * pgpPubKey.getValidSeconds());
}
X509Certificate selfSignedCert = createSelfSignedCert(
pgpPubKey.getKey(Constants.BOUNCY_CASTLE_PROVIDER_NAME), pgpPrivKey.getKey(),
x509name, creationTime, validTo, subjAltNameURI);
return selfSignedCert;
}
/**
* This is a password callback handler that will fill in a password automatically. Useful to
* configure passwords in advance, but should be used with caution depending on how much you
* allow passwords to be stored within your application.
*
* @author Bruno Harbulot.
*
*/
public final static class PredefinedPasswordCallbackHandler implements CallbackHandler {
private char[] password;
private String prompt;
public PredefinedPasswordCallbackHandler(String password) {
this(password == null ? null : password.toCharArray(), null);
}
public PredefinedPasswordCallbackHandler(char[] password) {
this(password, null);
}
public PredefinedPasswordCallbackHandler(String password, String prompt) {
this(password == null ? null : password.toCharArray(), prompt);
}
public PredefinedPasswordCallbackHandler(char[] password, String prompt) {
this.password = password;
this.prompt = prompt;
}
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof PasswordCallback) {
PasswordCallback pwCallback = (PasswordCallback) callback;
if ((this.prompt == null) || (this.prompt.equals(pwCallback.getPrompt()))) {
pwCallback.setPassword(this.password);
}
} else {
throw new UnsupportedCallbackException(callback, "Unrecognised callback.");
}
}
}
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
}

View File

@@ -0,0 +1,9 @@
package org.sufficientlysecure.keychain.pgp.exception;
public class NoAsymmetricEncryptionException extends Exception {
static final long serialVersionUID = 0xf812773343L;
public NoAsymmetricEncryptionException() {
super();
}
}

View File

@@ -0,0 +1,9 @@
package org.sufficientlysecure.keychain.pgp.exception;
public class PgpGeneralException extends Exception {
static final long serialVersionUID = 0xf812773342L;
public PgpGeneralException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,268 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.provider;
import org.sufficientlysecure.keychain.Constants;
import android.net.Uri;
import android.provider.BaseColumns;
public class KeychainContract {
interface KeyRingsColumns {
String MASTER_KEY_ID = "master_key_id"; // not a database id
String TYPE = "type"; // see KeyTypes
String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob
}
interface KeysColumns {
String KEY_ID = "key_id"; // not a database id
String TYPE = "type"; // see KeyTypes
String IS_MASTER_KEY = "is_master_key";
String ALGORITHM = "algorithm";
String KEY_SIZE = "key_size";
String CAN_CERTIFY = "can_certify";
String CAN_SIGN = "can_sign";
String CAN_ENCRYPT = "can_encrypt";
String IS_REVOKED = "is_revoked";
String CREATION = "creation";
String EXPIRY = "expiry";
String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID
String KEY_DATA = "key_data"; // PGPPublicKey/PGPSecretKey blob
String RANK = "rank";
}
interface UserIdsColumns {
String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID
String USER_ID = "user_id"; // not a database id
String RANK = "rank";
}
interface ApiAppsColumns {
String PACKAGE_NAME = "package_name";
String PACKAGE_SIGNATURE = "package_signature";
String KEY_ID = "key_id"; // not a database id
String ENCRYPTION_ALGORITHM = "encryption_algorithm";
String HASH_ALORITHM = "hash_algorithm";
String COMPRESSION = "compression";
}
public static final class KeyTypes {
public static final int PUBLIC = 0;
public static final int SECRET = 1;
}
public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".provider";
private static final Uri BASE_CONTENT_URI_INTERNAL = Uri
.parse("content://" + CONTENT_AUTHORITY);
public static final String BASE_KEY_RINGS = "key_rings";
public static final String BASE_DATA = "data";
public static final String PATH_PUBLIC = "public";
public static final String PATH_SECRET = "secret";
public static final String PATH_BY_MASTER_KEY_ID = "master_key_id";
public static final String PATH_BY_KEY_ID = "key_id";
public static final String PATH_BY_EMAILS = "emails";
public static final String PATH_BY_LIKE_EMAIL = "like_email";
public static final String PATH_USER_IDS = "user_ids";
public static final String PATH_KEYS = "keys";
public static final String BASE_API_APPS = "api_apps";
public static final String PATH_BY_PACKAGE_NAME = "package_name";
public static class KeyRings implements KeyRingsColumns, BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
.appendPath(BASE_KEY_RINGS).build();
/** Use if multiple items get returned */
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key_ring";
/** Use if a single item is returned */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key_ring";
public static Uri buildPublicKeyRingsUri() {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build();
}
public static Uri buildPublicKeyRingsUri(String keyRingRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId).build();
}
public static Uri buildPublicKeyRingsByMasterKeyIdUri(String masterKeyId) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC)
.appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build();
}
public static Uri buildPublicKeyRingsByKeyIdUri(String keyId) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_KEY_ID)
.appendPath(keyId).build();
}
public static Uri buildPublicKeyRingsByEmailsUri(String emails) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_EMAILS)
.appendPath(emails).build();
}
public static Uri buildPublicKeyRingsByLikeEmailUri(String emails) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_LIKE_EMAIL)
.appendPath(emails).build();
}
public static Uri buildSecretKeyRingsUri() {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build();
}
public static Uri buildSecretKeyRingsUri(String keyRingRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId).build();
}
public static Uri buildSecretKeyRingsByMasterKeyIdUri(String masterKeyId) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET)
.appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build();
}
public static Uri buildSecretKeyRingsByKeyIdUri(String keyId) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_KEY_ID)
.appendPath(keyId).build();
}
public static Uri buildSecretKeyRingsByEmailsUri(String emails) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_EMAILS)
.appendPath(emails).build();
}
public static Uri buildSecretKeyRingsByLikeEmails(String emails) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_LIKE_EMAIL)
.appendPath(emails).build();
}
}
public static class Keys implements KeysColumns, BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
.appendPath(BASE_KEY_RINGS).build();
/** Use if multiple items get returned */
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key";
/** Use if a single item is returned */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key";
public static Uri buildPublicKeysUri(String keyRingRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
.appendPath(PATH_KEYS).build();
}
public static Uri buildPublicKeysUri(String keyRingRowId, String keyRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
.appendPath(PATH_KEYS).appendPath(keyRowId).build();
}
public static Uri buildSecretKeysUri(String keyRingRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
.appendPath(PATH_KEYS).build();
}
public static Uri buildSecretKeysUri(String keyRingRowId, String keyRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
.appendPath(PATH_KEYS).appendPath(keyRowId).build();
}
public static Uri buildKeysUri(Uri keyRingUri) {
return keyRingUri.buildUpon().appendPath(PATH_KEYS).build();
}
public static Uri buildKeysUri(Uri keyRingUri, String keyRowId) {
return keyRingUri.buildUpon().appendPath(PATH_KEYS).appendPath(keyRowId).build();
}
}
public static class UserIds implements UserIdsColumns, BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
.appendPath(BASE_KEY_RINGS).build();
/** Use if multiple items get returned */
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.user_id";
/** Use if a single item is returned */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.user_id";
public static Uri buildPublicUserIdsUri(String keyRingRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
.appendPath(PATH_USER_IDS).build();
}
public static Uri buildPublicUserIdsUri(String keyRingRowId, String userIdRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
.appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
}
public static Uri buildSecretUserIdsUri(String keyRingRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
.appendPath(PATH_USER_IDS).build();
}
public static Uri buildSecretUserIdsUri(String keyRingRowId, String userIdRowId) {
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
.appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
}
public static Uri buildUserIdsUri(Uri keyRingUri) {
return keyRingUri.buildUpon().appendPath(PATH_USER_IDS).build();
}
public static Uri buildUserIdsUri(Uri keyRingUri, String userIdRowId) {
return keyRingUri.buildUpon().appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
}
}
public static class ApiApps implements ApiAppsColumns, BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
.appendPath(BASE_API_APPS).build();
/** Use if multiple items get returned */
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.api_apps";
/** Use if a single item is returned */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.api_apps";
public static Uri buildIdUri(String rowId) {
return CONTENT_URI.buildUpon().appendPath(rowId).build();
}
public static Uri buildByPackageNameUri(String packageName) {
return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName)
.build();
}
}
public static class DataStream {
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
.appendPath(BASE_DATA).build();
public static Uri buildDataStreamUri(String streamFilename) {
return CONTENT_URI.buildUpon().appendPath(streamFilename).build();
}
}
private KeychainContract() {
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.provider;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
public class KeychainDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "apg.db";
private static final int DATABASE_VERSION = 6;
public interface Tables {
String KEY_RINGS = "key_rings";
String KEYS = "keys";
String USER_IDS = "user_ids";
String API_APPS = "api_apps";
}
private static final String CREATE_KEY_RINGS = "CREATE TABLE IF NOT EXISTS " + Tables.KEY_RINGS
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ KeyRingsColumns.MASTER_KEY_ID + " INT64, " + KeyRingsColumns.TYPE + " INTEGER, "
+ KeyRingsColumns.KEY_RING_DATA + " BLOB)";
private static final String CREATE_KEYS = "CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " ("
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + KeysColumns.KEY_ID
+ " INT64, " + KeysColumns.TYPE + " INTEGER, " + KeysColumns.IS_MASTER_KEY
+ " INTEGER, " + KeysColumns.ALGORITHM + " INTEGER, " + KeysColumns.KEY_SIZE
+ " INTEGER, " + KeysColumns.CAN_CERTIFY + " INTEGER, " + KeysColumns.CAN_SIGN
+ " INTEGER, " + KeysColumns.CAN_ENCRYPT + " INTEGER, " + KeysColumns.IS_REVOKED
+ " INTEGER, " + KeysColumns.CREATION + " INTEGER, " + KeysColumns.EXPIRY
+ " INTEGER, " + KeysColumns.KEY_DATA + " BLOB," + KeysColumns.RANK + " INTEGER, "
+ KeysColumns.KEY_RING_ROW_ID + " INTEGER NOT NULL, FOREIGN KEY("
+ KeysColumns.KEY_RING_ROW_ID + ") REFERENCES " + Tables.KEY_RINGS + "("
+ BaseColumns._ID + ") ON DELETE CASCADE)";
private static final String CREATE_USER_IDS = "CREATE TABLE IF NOT EXISTS " + Tables.USER_IDS
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ UserIdsColumns.USER_ID + " TEXT, " + UserIdsColumns.RANK + " INTEGER, "
+ UserIdsColumns.KEY_RING_ROW_ID + " INTEGER NOT NULL, FOREIGN KEY("
+ UserIdsColumns.KEY_RING_ROW_ID + ") REFERENCES " + Tables.KEY_RINGS + "("
+ BaseColumns._ID + ") ON DELETE CASCADE)";
private static final String CREATE_API_APPS = "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ ApiAppsColumns.PACKAGE_NAME + " TEXT UNIQUE, " + ApiAppsColumns.PACKAGE_SIGNATURE
+ " BLOB, " + ApiAppsColumns.KEY_ID + " INT64, " + ApiAppsColumns.ENCRYPTION_ALGORITHM
+ " INTEGER, " + ApiAppsColumns.HASH_ALORITHM + " INTEGER, "
+ ApiAppsColumns.COMPRESSION + " INTEGER)";
KeychainDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.w(Constants.TAG, "Creating database...");
db.execSQL(CREATE_KEY_RINGS);
db.execSQL(CREATE_KEYS);
db.execSQL(CREATE_USER_IDS);
db.execSQL(CREATE_API_APPS);
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
if (!db.isReadOnly()) {
// Enable foreign key constraints
db.execSQL("PRAGMA foreign_keys=ON;");
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(Constants.TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
// Upgrade from oldVersion through all methods to newest one
for (int version = oldVersion; version < newVersion; ++version) {
Log.w(Constants.TAG, "Upgrading database to version " + version);
switch (version) {
case 3:
db.execSQL("ALTER TABLE " + Tables.KEYS + " ADD COLUMN " + KeysColumns.CAN_CERTIFY
+ " INTEGER DEFAULT 0;");
db.execSQL("UPDATE " + Tables.KEYS + " SET " + KeysColumns.CAN_CERTIFY
+ " = 1 WHERE " + KeysColumns.IS_MASTER_KEY + "= 1;");
break;
case 4:
db.execSQL(CREATE_API_APPS);
case 5:
// new column: package_signature
db.execSQL("DROP TABLE IF EXISTS " + Tables.API_APPS);
db.execSQL(CREATE_API_APPS);
default:
break;
}
}
}
}

View File

@@ -0,0 +1,971 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.provider;
import java.util.Arrays;
import java.util.HashMap;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyTypes;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.util.Log;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
public class KeychainProvider extends ContentProvider {
// public static final String ACTION_BROADCAST_DATABASE_CHANGE = Constants.PACKAGE_NAME
// + ".action.DATABASE_CHANGE";
//
// public static final String EXTRA_BROADCAST_KEY_TYPE = "key_type";
// public static final String EXTRA_BROADCAST_CONTENT_ITEM_TYPE = "contentItemType";
private static final int PUBLIC_KEY_RING = 101;
private static final int PUBLIC_KEY_RING_BY_ROW_ID = 102;
private static final int PUBLIC_KEY_RING_BY_MASTER_KEY_ID = 103;
private static final int PUBLIC_KEY_RING_BY_KEY_ID = 104;
private static final int PUBLIC_KEY_RING_BY_EMAILS = 105;
private static final int PUBLIC_KEY_RING_BY_LIKE_EMAIL = 106;
private static final int PUBLIC_KEY_RING_KEY = 111;
private static final int PUBLIC_KEY_RING_KEY_BY_ROW_ID = 112;
private static final int PUBLIC_KEY_RING_USER_ID = 121;
private static final int PUBLIC_KEY_RING_USER_ID_BY_ROW_ID = 122;
private static final int SECRET_KEY_RING = 201;
private static final int SECRET_KEY_RING_BY_ROW_ID = 202;
private static final int SECRET_KEY_RING_BY_MASTER_KEY_ID = 203;
private static final int SECRET_KEY_RING_BY_KEY_ID = 204;
private static final int SECRET_KEY_RING_BY_EMAILS = 205;
private static final int SECRET_KEY_RING_BY_LIKE_EMAIL = 206;
private static final int SECRET_KEY_RING_KEY = 211;
private static final int SECRET_KEY_RING_KEY_BY_ROW_ID = 212;
private static final int SECRET_KEY_RING_USER_ID = 221;
private static final int SECRET_KEY_RING_USER_ID_BY_ROW_ID = 222;
private static final int API_APPS = 301;
private static final int API_APPS_BY_ROW_ID = 302;
private static final int API_APPS_BY_PACKAGE_NAME = 303;
// private static final int DATA_STREAM = 401;
protected UriMatcher mUriMatcher;
/**
* Build and return a {@link UriMatcher} that catches all {@link Uri} variations supported by
* this {@link ContentProvider}.
*/
protected UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
String authority = KeychainContract.CONTENT_AUTHORITY;
/**
* public key rings
*
* <pre>
* key_rings/public
* key_rings/public/#
* key_rings/public/master_key_id/_
* key_rings/public/key_id/_
* key_rings/public/emails/_
* key_rings/public/like_email/_
* </pre>
*/
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC, PUBLIC_KEY_RING);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/#", PUBLIC_KEY_RING_BY_ROW_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_MASTER_KEY_ID
+ "/*", PUBLIC_KEY_RING_BY_MASTER_KEY_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_KEY_ID + "/*",
PUBLIC_KEY_RING_BY_KEY_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_EMAILS + "/*",
PUBLIC_KEY_RING_BY_EMAILS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_EMAILS,
PUBLIC_KEY_RING_BY_EMAILS); // without emails specified
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_LIKE_EMAIL + "/*",
PUBLIC_KEY_RING_BY_LIKE_EMAIL);
/**
* public keys
*
* <pre>
* key_rings/public/#/keys
* key_rings/public/#/keys/#
* </pre>
*/
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_KEYS,
PUBLIC_KEY_RING_KEY);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_KEYS + "/#",
PUBLIC_KEY_RING_KEY_BY_ROW_ID);
/**
* public user ids
*
* <pre>
* key_rings/public/#/user_ids
* key_rings/public/#/user_ids/#
* </pre>
*/
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_USER_IDS,
PUBLIC_KEY_RING_USER_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_USER_IDS + "/#",
PUBLIC_KEY_RING_USER_ID_BY_ROW_ID);
/**
* secret key rings
*
* <pre>
* key_rings/secret
* key_rings/secret/#
* key_rings/secret/master_key_id/_
* key_rings/secret/key_id/_
* key_rings/secret/emails/_
* key_rings/secret/like_email/_
* </pre>
*/
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET, SECRET_KEY_RING);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/#", SECRET_KEY_RING_BY_ROW_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_MASTER_KEY_ID
+ "/*", SECRET_KEY_RING_BY_MASTER_KEY_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_KEY_ID + "/*",
SECRET_KEY_RING_BY_KEY_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_EMAILS + "/*",
SECRET_KEY_RING_BY_EMAILS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_EMAILS,
SECRET_KEY_RING_BY_EMAILS); // without emails specified
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_LIKE_EMAIL + "/*",
SECRET_KEY_RING_BY_LIKE_EMAIL);
/**
* secret keys
*
* <pre>
* key_rings/secret/#/keys
* key_rings/secret/#/keys/#
* </pre>
*/
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_KEYS,
SECRET_KEY_RING_KEY);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_KEYS + "/#",
SECRET_KEY_RING_KEY_BY_ROW_ID);
/**
* secret user ids
*
* <pre>
* key_rings/secret/#/user_ids
* key_rings/secret/#/user_ids/#
* </pre>
*/
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_USER_IDS,
SECRET_KEY_RING_USER_ID);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_USER_IDS + "/#",
SECRET_KEY_RING_USER_ID_BY_ROW_ID);
/**
* API apps
*/
matcher.addURI(authority, KeychainContract.BASE_API_APPS, API_APPS);
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/#", API_APPS_BY_ROW_ID);
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/"
+ KeychainContract.PATH_BY_PACKAGE_NAME + "/*", API_APPS_BY_PACKAGE_NAME);
/**
* data stream
*
* <pre>
* data / _
* </pre>
*/
// matcher.addURI(authority, KeychainContract.BASE_DATA + "/*", DATA_STREAM);
return matcher;
}
private KeychainDatabase mApgDatabase;
/** {@inheritDoc} */
@Override
public boolean onCreate() {
mUriMatcher = buildUriMatcher();
mApgDatabase = new KeychainDatabase(getContext());
return true;
}
/** {@inheritDoc} */
@Override
public String getType(Uri uri) {
final int match = mUriMatcher.match(uri);
switch (match) {
case PUBLIC_KEY_RING:
case PUBLIC_KEY_RING_BY_EMAILS:
case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
case SECRET_KEY_RING:
case SECRET_KEY_RING_BY_EMAILS:
case SECRET_KEY_RING_BY_LIKE_EMAIL:
return KeyRings.CONTENT_TYPE;
case PUBLIC_KEY_RING_BY_ROW_ID:
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
case PUBLIC_KEY_RING_BY_KEY_ID:
case SECRET_KEY_RING_BY_ROW_ID:
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
case SECRET_KEY_RING_BY_KEY_ID:
return KeyRings.CONTENT_ITEM_TYPE;
case PUBLIC_KEY_RING_KEY:
case SECRET_KEY_RING_KEY:
return Keys.CONTENT_TYPE;
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
case SECRET_KEY_RING_KEY_BY_ROW_ID:
return Keys.CONTENT_ITEM_TYPE;
case PUBLIC_KEY_RING_USER_ID:
case SECRET_KEY_RING_USER_ID:
return UserIds.CONTENT_TYPE;
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
return UserIds.CONTENT_ITEM_TYPE;
case API_APPS:
return ApiApps.CONTENT_TYPE;
case API_APPS_BY_ROW_ID:
case API_APPS_BY_PACKAGE_NAME:
return ApiApps.CONTENT_ITEM_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
/**
* Returns type of the query (secret/public)
*
* @param uri
* @return
*/
private int getKeyType(int match) {
int type;
switch (match) {
case PUBLIC_KEY_RING:
case PUBLIC_KEY_RING_BY_ROW_ID:
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
case PUBLIC_KEY_RING_BY_KEY_ID:
case PUBLIC_KEY_RING_BY_EMAILS:
case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
case PUBLIC_KEY_RING_KEY:
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
case PUBLIC_KEY_RING_USER_ID:
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
type = KeyTypes.PUBLIC;
break;
case SECRET_KEY_RING:
case SECRET_KEY_RING_BY_ROW_ID:
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
case SECRET_KEY_RING_BY_KEY_ID:
case SECRET_KEY_RING_BY_EMAILS:
case SECRET_KEY_RING_BY_LIKE_EMAIL:
case SECRET_KEY_RING_KEY:
case SECRET_KEY_RING_KEY_BY_ROW_ID:
case SECRET_KEY_RING_USER_ID:
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
type = KeyTypes.SECRET;
break;
default:
Log.e(Constants.TAG, "Unknown match " + match);
type = -1;
break;
}
return type;
}
/**
* Set result of query to specific columns, don't show blob column for external content provider
*
* @return
*/
private HashMap<String, String> getProjectionMapForKeyRings() {
HashMap<String, String> projectionMap = new HashMap<String, String>();
projectionMap.put(BaseColumns._ID, Tables.KEY_RINGS + "." + BaseColumns._ID);
projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEY_RINGS + "."
+ KeyRingsColumns.MASTER_KEY_ID);
projectionMap.put(KeyRingsColumns.KEY_RING_DATA, Tables.KEY_RINGS + "."
+ KeyRingsColumns.KEY_RING_DATA);
projectionMap.put(UserIdsColumns.USER_ID, Tables.USER_IDS + "." + UserIdsColumns.USER_ID);
return projectionMap;
}
/**
* Set result of query to specific columns, don't show blob column for external content provider
*
* @return
*/
private HashMap<String, String> getProjectionMapForKeys() {
HashMap<String, String> projectionMap = new HashMap<String, String>();
projectionMap.put(BaseColumns._ID, BaseColumns._ID);
projectionMap.put(KeysColumns.KEY_ID, KeysColumns.KEY_ID);
projectionMap.put(KeysColumns.IS_MASTER_KEY, KeysColumns.IS_MASTER_KEY);
projectionMap.put(KeysColumns.ALGORITHM, KeysColumns.ALGORITHM);
projectionMap.put(KeysColumns.KEY_SIZE, KeysColumns.KEY_SIZE);
projectionMap.put(KeysColumns.CAN_CERTIFY, KeysColumns.CAN_CERTIFY);
projectionMap.put(KeysColumns.CAN_SIGN, KeysColumns.CAN_SIGN);
projectionMap.put(KeysColumns.CAN_ENCRYPT, KeysColumns.CAN_ENCRYPT);
projectionMap.put(KeysColumns.IS_REVOKED, KeysColumns.IS_REVOKED);
projectionMap.put(KeysColumns.CREATION, KeysColumns.CREATION);
projectionMap.put(KeysColumns.EXPIRY, KeysColumns.EXPIRY);
projectionMap.put(KeysColumns.KEY_RING_ROW_ID, KeysColumns.KEY_RING_ROW_ID);
projectionMap.put(KeysColumns.KEY_DATA, KeysColumns.KEY_DATA);
projectionMap.put(KeysColumns.RANK, KeysColumns.RANK);
return projectionMap;
}
/**
* Builds default query for keyRings: KeyRings table is joined with UserIds
*
* @param qb
* @param match
* @param isMasterKey
* @param sortOrder
* @return
*/
private SQLiteQueryBuilder buildKeyRingQuery(SQLiteQueryBuilder qb, int match, String sortOrder) {
// public or secret keyring
qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = ");
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
// join keyrings with userIds to every keyring
qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.USER_IDS + " ON " + "("
+ Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "."
+ UserIdsColumns.KEY_RING_ROW_ID + " AND " + Tables.USER_IDS + "."
+ UserIdsColumns.RANK + " = '0')");
qb.setProjectionMap(getProjectionMapForKeyRings());
return qb;
}
/**
* Builds default query for keyRings: KeyRings table is joined with Keys and UserIds
*
* @param qb
* @param match
* @param isMasterKey
* @param sortOrder
* @return
*/
private SQLiteQueryBuilder buildKeyRingQueryWithKeys(SQLiteQueryBuilder qb, int match,
String sortOrder) {
// public or secret keyring
qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = ");
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
// join keyrings with keys and userIds to every keyring
qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.KEYS + " ON " + "("
+ Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.KEYS + "."
+ KeysColumns.KEY_RING_ROW_ID + ") " + " INNER JOIN " + Tables.USER_IDS + " ON "
+ "(" + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "."
+ UserIdsColumns.KEY_RING_ROW_ID + " AND " + Tables.USER_IDS + "."
+ UserIdsColumns.RANK + " = '0')");
qb.setProjectionMap(getProjectionMapForKeyRings());
return qb;
}
/** {@inheritDoc} */
@SuppressWarnings("deprecation")
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Log.v(Constants.TAG, "query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")");
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
SQLiteDatabase db = mApgDatabase.getReadableDatabase();
int match = mUriMatcher.match(uri);
switch (match) {
case PUBLIC_KEY_RING:
case SECRET_KEY_RING:
qb = buildKeyRingQuery(qb, match, sortOrder);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
}
break;
case PUBLIC_KEY_RING_BY_ROW_ID:
case SECRET_KEY_RING_BY_ROW_ID:
qb = buildKeyRingQuery(qb, match, sortOrder);
qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + BaseColumns._ID + " = ");
qb.appendWhereEscapeString(uri.getLastPathSegment());
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
}
break;
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
qb = buildKeyRingQuery(qb, match, sortOrder);
qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getLastPathSegment());
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
}
break;
case SECRET_KEY_RING_BY_KEY_ID:
case PUBLIC_KEY_RING_BY_KEY_ID:
qb = buildKeyRingQueryWithKeys(qb, match, sortOrder);
qb.appendWhere(" AND " + Tables.KEYS + "." + KeysColumns.KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getLastPathSegment());
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
}
break;
case SECRET_KEY_RING_BY_EMAILS:
case PUBLIC_KEY_RING_BY_EMAILS:
qb = buildKeyRingQuery(qb, match, sortOrder);
String emails = uri.getLastPathSegment();
String chunks[] = emails.split(" *, *");
boolean gotCondition = false;
String emailWhere = "";
for (int i = 0; i < chunks.length; ++i) {
if (chunks[i].length() == 0) {
continue;
}
if (i != 0) {
emailWhere += " OR ";
}
emailWhere += "tmp." + UserIdsColumns.USER_ID + " LIKE ";
// match '*<email>', so it has to be at the *end* of the user id
emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">");
gotCondition = true;
}
if (gotCondition) {
qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM "
+ Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID
+ " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + emailWhere
+ "))");
}
break;
case SECRET_KEY_RING_BY_LIKE_EMAIL:
case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
qb = buildKeyRingQuery(qb, match, sortOrder);
String likeEmail = uri.getLastPathSegment();
String likeEmailWhere = "tmp." + UserIdsColumns.USER_ID + " LIKE "
+ DatabaseUtils.sqlEscapeString("%<%" + likeEmail + "%>");
qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM "
+ Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID
+ " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + likeEmailWhere
+ "))");
break;
case PUBLIC_KEY_RING_KEY:
case SECRET_KEY_RING_KEY:
qb.setTables(Tables.KEYS);
qb.appendWhere(KeysColumns.TYPE + " = ");
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
qb.setProjectionMap(getProjectionMapForKeys());
break;
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
case SECRET_KEY_RING_KEY_BY_ROW_ID:
qb.setTables(Tables.KEYS);
qb.appendWhere(KeysColumns.TYPE + " = ");
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
qb.appendWhere(" AND " + BaseColumns._ID + " = ");
qb.appendWhereEscapeString(uri.getLastPathSegment());
qb.setProjectionMap(getProjectionMapForKeys());
break;
case PUBLIC_KEY_RING_USER_ID:
case SECRET_KEY_RING_USER_ID:
qb.setTables(Tables.USER_IDS);
qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
break;
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
qb.setTables(Tables.USER_IDS);
qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
qb.appendWhere(" AND " + BaseColumns._ID + " = ");
qb.appendWhereEscapeString(uri.getLastPathSegment());
break;
case API_APPS:
qb.setTables(Tables.API_APPS);
break;
case API_APPS_BY_ROW_ID:
qb.setTables(Tables.API_APPS);
qb.appendWhere(BaseColumns._ID + " = ");
qb.appendWhereEscapeString(uri.getLastPathSegment());
break;
case API_APPS_BY_PACKAGE_NAME:
qb.setTables(Tables.API_APPS);
qb.appendWhere(ApiApps.PACKAGE_NAME + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// If no sort order is specified use the default
String orderBy;
if (TextUtils.isEmpty(sortOrder)) {
orderBy = null;
} else {
orderBy = sortOrder;
}
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
// Tell the cursor what uri to watch, so it knows when its source data changes
c.setNotificationUri(getContext().getContentResolver(), uri);
if (Constants.DEBUG) {
Log.d(Constants.TAG,
"Query: "
+ qb.buildQuery(projection, selection, selectionArgs, null, null,
orderBy, null));
Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(c));
}
return c;
}
/** {@inheritDoc} */
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.d(Constants.TAG, "insert(uri=" + uri + ", values=" + values.toString() + ")");
final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
Uri rowUri = null;
long rowId = -1;
try {
final int match = mUriMatcher.match(uri);
switch (match) {
case PUBLIC_KEY_RING:
values.put(KeyRings.TYPE, KeyTypes.PUBLIC);
rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values);
rowUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId));
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_KEY:
values.put(Keys.TYPE, KeyTypes.PUBLIC);
rowId = db.insertOrThrow(Tables.KEYS, null, values);
rowUri = Keys.buildPublicKeysUri(Long.toString(rowId));
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_USER_ID:
rowId = db.insertOrThrow(Tables.USER_IDS, null, values);
rowUri = UserIds.buildPublicUserIdsUri(Long.toString(rowId));
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case SECRET_KEY_RING:
values.put(KeyRings.TYPE, KeyTypes.SECRET);
rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values);
rowUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId));
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case SECRET_KEY_RING_KEY:
values.put(Keys.TYPE, KeyTypes.SECRET);
rowId = db.insertOrThrow(Tables.KEYS, null, values);
rowUri = Keys.buildSecretKeysUri(Long.toString(rowId));
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case SECRET_KEY_RING_USER_ID:
rowId = db.insertOrThrow(Tables.USER_IDS, null, values);
rowUri = UserIds.buildSecretUserIdsUri(Long.toString(rowId));
break;
case API_APPS:
rowId = db.insertOrThrow(Tables.API_APPS, null, values);
rowUri = ApiApps.buildIdUri(Long.toString(rowId));
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
// notify of changes in db
getContext().getContentResolver().notifyChange(uri, null);
} catch (SQLiteConstraintException e) {
Log.e(Constants.TAG, "Constraint exception on insert! Entry already existing?");
}
return rowUri;
}
/** {@inheritDoc} */
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.v(Constants.TAG, "delete(uri=" + uri + ")");
final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
int count;
final int match = mUriMatcher.match(uri);
String defaultSelection = null;
switch (match) {
case PUBLIC_KEY_RING_BY_ROW_ID:
case SECRET_KEY_RING_BY_ROW_ID:
defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment();
// corresponding keys and userIds are deleted by ON DELETE CASCADE
count = db.delete(Tables.KEY_RINGS,
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection),
selectionArgs);
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment();
// corresponding keys and userIds are deleted by ON DELETE CASCADE
count = db.delete(Tables.KEY_RINGS,
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection),
selectionArgs);
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
case SECRET_KEY_RING_KEY_BY_ROW_ID:
count = db.delete(Tables.KEYS,
buildDefaultKeysSelection(uri, getKeyType(match), selection), selectionArgs);
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
count = db.delete(Tables.KEYS, buildDefaultUserIdsSelection(uri, selection),
selectionArgs);
break;
case API_APPS_BY_ROW_ID:
count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, false, selection),
selectionArgs);
break;
case API_APPS_BY_PACKAGE_NAME:
count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, true, selection),
selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
// notify of changes in db
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
/** {@inheritDoc} */
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Log.v(Constants.TAG, "update(uri=" + uri + ", values=" + values.toString() + ")");
final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
String defaultSelection = null;
int count = 0;
try {
final int match = mUriMatcher.match(uri);
switch (match) {
case PUBLIC_KEY_RING_BY_ROW_ID:
case SECRET_KEY_RING_BY_ROW_ID:
defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment();
count = db.update(
Tables.KEY_RINGS,
values,
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match),
selection), selectionArgs);
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment();
count = db.update(
Tables.KEY_RINGS,
values,
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match),
selection), selectionArgs);
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
case SECRET_KEY_RING_KEY_BY_ROW_ID:
count = db
.update(Tables.KEYS, values,
buildDefaultKeysSelection(uri, getKeyType(match), selection),
selectionArgs);
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
break;
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
count = db.update(Tables.USER_IDS, values,
buildDefaultUserIdsSelection(uri, selection), selectionArgs);
break;
case API_APPS_BY_ROW_ID:
count = db.update(Tables.API_APPS, values,
buildDefaultApiAppsSelection(uri, false, selection), selectionArgs);
break;
case API_APPS_BY_PACKAGE_NAME:
count = db.update(Tables.API_APPS, values,
buildDefaultApiAppsSelection(uri, true, selection), selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
// notify of changes in db
getContext().getContentResolver().notifyChange(uri, null);
} catch (SQLiteConstraintException e) {
Log.e(Constants.TAG, "Constraint exception on update! Entry already existing?");
}
return count;
}
/**
* Build default selection statement for KeyRings. If no extra selection is specified only build
* where clause with rowId
*
* @param uri
* @param selection
* @return
*/
private String buildDefaultKeyRingsSelection(String defaultSelection, Integer keyType,
String selection) {
String andType = "";
if (keyType != null) {
andType = " AND " + KeyRingsColumns.TYPE + "=" + keyType;
}
String andSelection = "";
if (!TextUtils.isEmpty(selection)) {
andSelection = " AND (" + selection + ")";
}
return defaultSelection + andType + andSelection;
}
/**
* Build default selection statement for Keys. If no extra selection is specified only build
* where clause with rowId
*
* @param uri
* @param selection
* @return
*/
private String buildDefaultKeysSelection(Uri uri, Integer keyType, String selection) {
String rowId = uri.getLastPathSegment();
String foreignKeyRingRowId = uri.getPathSegments().get(2);
String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = "
+ foreignKeyRingRowId;
String andType = "";
if (keyType != null) {
andType = " AND " + KeysColumns.TYPE + "=" + keyType;
}
String andSelection = "";
if (!TextUtils.isEmpty(selection)) {
andSelection = " AND (" + selection + ")";
}
return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andType + andSelection;
}
/**
* Build default selection statement for UserIds. If no extra selection is specified only build
* where clause with rowId
*
* @param uri
* @param selection
* @return
*/
private String buildDefaultUserIdsSelection(Uri uri, String selection) {
String rowId = uri.getLastPathSegment();
String foreignKeyRingRowId = uri.getPathSegments().get(2);
String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = "
+ foreignKeyRingRowId;
String andSelection = "";
if (!TextUtils.isEmpty(selection)) {
andSelection = " AND (" + selection + ")";
}
return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andSelection;
}
/**
* Build default selection statement for API apps. If no extra selection is specified only build
* where clause with rowId
*
* @param uri
* @param selection
* @return
*/
private String buildDefaultApiAppsSelection(Uri uri, boolean packageSelection, String selection) {
String lastPathSegment = uri.getLastPathSegment();
String andSelection = "";
if (!TextUtils.isEmpty(selection)) {
andSelection = " AND (" + selection + ")";
}
if (packageSelection) {
return ApiApps.PACKAGE_NAME + "=" + lastPathSegment + andSelection;
} else {
return BaseColumns._ID + "=" + lastPathSegment + andSelection;
}
}
// @Override
// public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
// int match = mUriMatcher.match(uri);
// if (match != DATA_STREAM) {
// throw new FileNotFoundException();
// }
// String fileName = uri.getLastPathSegment();
// File file = new File(getContext().getFilesDir().getAbsolutePath(), fileName);
// return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
// }
/**
* This broadcast is send system wide to inform other application that a keyring was inserted,
* updated, or deleted
*/
private void sendBroadcastDatabaseChange(int keyType, String contentItemType) {
// TODO: Disabled, old API
// Intent intent = new Intent();
// intent.setAction(ACTION_BROADCAST_DATABASE_CHANGE);
// intent.putExtra(EXTRA_BROADCAST_KEY_TYPE, keyType);
// intent.putExtra(EXTRA_BROADCAST_CONTENT_ITEM_TYPE, contentItemType);
//
// getContext().sendBroadcast(intent, Constants.PERMISSION_ACCESS_API);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.provider;
import org.sufficientlysecure.keychain.Constants;
import android.net.Uri;
import android.provider.BaseColumns;
public class KeychainServiceBlobContract {
interface BlobsColumns {
String KEY = "key";
}
public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".blobs";
private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static class Blobs implements BlobsColumns, BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI;
}
private KeychainServiceBlobContract() {
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.provider;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
public class KeychainServiceBlobDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "apg_blob.db";
private static final int DATABASE_VERSION = 2;
public static final String TABLE = "data";
public KeychainServiceBlobDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE + " ( " + BaseColumns._ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT, " + BlobsColumns.KEY + " TEXT NOT NULL)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// no upgrade necessary yet
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.provider;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.Blobs;
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
import org.sufficientlysecure.keychain.util.Log;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
public class KeychainServiceBlobProvider extends ContentProvider {
private static final String STORE_PATH = Constants.path.APP_DIR + "/ApgBlobs";
private KeychainServiceBlobDatabase mBlobDatabase = null;
public KeychainServiceBlobProvider() {
File dir = new File(STORE_PATH);
dir.mkdirs();
}
@Override
public boolean onCreate() {
mBlobDatabase = new KeychainServiceBlobDatabase(getContext());
return true;
}
/** {@inheritDoc} */
@Override
public Uri insert(Uri uri, ContentValues ignored) {
// ContentValues are actually ignored, because we want to store a blob with no more
// information but have to create an record with the password generated here first
ContentValues vals = new ContentValues();
// Insert a random key in the database. This has to provided by the caller when updating or
// getting the blob
String password = UUID.randomUUID().toString();
vals.put(BlobsColumns.KEY, password);
SQLiteDatabase db = mBlobDatabase.getWritableDatabase();
long newRowId = db.insert(KeychainServiceBlobDatabase.TABLE, null, vals);
Uri insertedUri = ContentUris.withAppendedId(Blobs.CONTENT_URI, newRowId);
return Uri.withAppendedPath(insertedUri, password);
}
/** {@inheritDoc} */
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws SecurityException,
FileNotFoundException {
Log.d(Constants.TAG, "openFile() called with uri: " + uri.toString() + " and mode: " + mode);
List<String> segments = uri.getPathSegments();
if (segments.size() < 2) {
throw new SecurityException("Password not found in URI");
}
String id = segments.get(0);
String key = segments.get(1);
Log.d(Constants.TAG, "Got id: " + id + " and key: " + key);
// get the data
SQLiteDatabase db = mBlobDatabase.getReadableDatabase();
Cursor result = db.query(KeychainServiceBlobDatabase.TABLE, new String[] { BaseColumns._ID },
BaseColumns._ID + " = ? and " + BlobsColumns.KEY + " = ?",
new String[] { id, key }, null, null, null);
if (result.getCount() == 0) {
// either the key is wrong or no id exists
throw new FileNotFoundException("No file found with that ID and/or password");
}
File targetFile = new File(STORE_PATH, id);
if (mode.equals("w")) {
Log.d(Constants.TAG, "Try to open file w");
if (!targetFile.exists()) {
try {
targetFile.createNewFile();
} catch (IOException e) {
Log.e(Constants.TAG, "Got IEOException on creating new file", e);
throw new FileNotFoundException("Could not create file to write to");
}
}
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_TRUNCATE);
} else if (mode.equals("r")) {
Log.d(Constants.TAG, "Try to open file r");
if (!targetFile.exists()) {
throw new FileNotFoundException("Error: Could not find the file requested");
}
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY);
}
return null;
}
/** {@inheritDoc} */
@Override
public String getType(Uri uri) {
return null;
}
/** {@inheritDoc} */
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
return null;
}
/** {@inheritDoc} */
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
/** {@inheritDoc} */
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}

View File

@@ -0,0 +1,809 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.provider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import org.spongycastle.bcpg.ArmoredOutputStream;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.service.remote.AppSettings;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.RemoteException;
public class ProviderHelper {
/**
* Private helper method to get PGPKeyRing from database
*
* @param context
* @param queryUri
* @return
*/
public static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) {
Cursor cursor = context.getContentResolver().query(queryUri,
new String[] { KeyRings._ID, KeyRings.KEY_RING_DATA }, null, null, null);
PGPKeyRing keyRing = null;
if (cursor != null && cursor.moveToFirst()) {
int keyRingDataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
byte[] data = cursor.getBlob(keyRingDataCol);
if (data != null) {
keyRing = PgpConversionHelper.BytesToPGPKeyRing(data);
}
}
if (cursor != null) {
cursor.close();
}
return keyRing;
}
/**
* Retrieves the actual PGPPublicKeyRing object from the database blob based on the rowId
*
* @param context
* @param rowId
* @return
*/
public static PGPPublicKeyRing getPGPPublicKeyRingByRowId(Context context, long rowId) {
Uri queryUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId));
return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
}
/**
* Retrieves the actual PGPPublicKeyRing object from the database blob based on the maserKeyId
*
* @param context
* @param masterKeyId
* @return
*/
public static PGPPublicKeyRing getPGPPublicKeyRingByMasterKeyId(Context context,
long masterKeyId) {
Uri queryUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
}
/**
* Retrieves the actual PGPPublicKeyRing object from the database blob associated with a key
* with this keyId
*
* @param context
* @param keyId
* @return
*/
public static PGPPublicKeyRing getPGPPublicKeyRingByKeyId(Context context, long keyId) {
Uri queryUri = KeyRings.buildPublicKeyRingsByKeyIdUri(Long.toString(keyId));
return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
}
/**
* Retrieves the actual PGPPublicKey object from the database blob associated with a key with
* this keyId
*
* @param context
* @param keyId
* @return
*/
public static PGPPublicKey getPGPPublicKeyByKeyId(Context context, long keyId) {
PGPPublicKeyRing keyRing = getPGPPublicKeyRingByKeyId(context, keyId);
if (keyRing == null) {
return null;
}
return keyRing.getPublicKey(keyId);
}
/**
* Retrieves the actual PGPSecretKeyRing object from the database blob based on the rowId
*
* @param context
* @param rowId
* @return
*/
public static PGPSecretKeyRing getPGPSecretKeyRingByRowId(Context context, long rowId) {
Uri queryUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId));
return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
}
/**
* Retrieves the actual PGPSecretKeyRing object from the database blob based on the maserKeyId
*
* @param context
* @param masterKeyId
* @return
*/
public static PGPSecretKeyRing getPGPSecretKeyRingByMasterKeyId(Context context,
long masterKeyId) {
Uri queryUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
}
/**
* Retrieves the actual PGPSecretKeyRing object from the database blob associated with a key
* with this keyId
*
* @param context
* @param keyId
* @return
*/
public static PGPSecretKeyRing getPGPSecretKeyRingByKeyId(Context context, long keyId) {
Uri queryUri = KeyRings.buildSecretKeyRingsByKeyIdUri(Long.toString(keyId));
return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
}
/**
* Retrieves the actual PGPSecretKey object from the database blob associated with a key with
* this keyId
*
* @param context
* @param keyId
* @return
*/
public static PGPSecretKey getPGPSecretKeyByKeyId(Context context, long keyId) {
PGPSecretKeyRing keyRing = getPGPSecretKeyRingByKeyId(context, keyId);
if (keyRing == null) {
return null;
}
return keyRing.getSecretKey(keyId);
}
/**
* Saves PGPPublicKeyRing with its keys and userIds in DB
*
* @param context
* @param keyRing
* @return
* @throws IOException
* @throws GeneralException
*/
@SuppressWarnings("unchecked")
public static void saveKeyRing(Context context, PGPPublicKeyRing keyRing) throws IOException {
PGPPublicKey masterKey = keyRing.getPublicKey();
long masterKeyId = masterKey.getKeyID();
// delete old version of this keyRing, which also deletes all keys and userIds on cascade
Uri deleteUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
try {
context.getContentResolver().delete(deleteUri, null, null);
} catch (UnsupportedOperationException e) {
Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e);
}
ContentValues values = new ContentValues();
values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
// insert new version of this keyRing
Uri uri = KeyRings.buildPublicKeyRingsUri();
Uri insertedUri = context.getContentResolver().insert(uri, values);
long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment());
// save all keys and userIds included in keyRing object in database
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
int rank = 0;
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
operations.add(buildPublicKeyOperations(context, keyRingRowId, key, rank));
++rank;
}
int userIdRank = 0;
for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
operations.add(buildPublicUserIdOperations(context, keyRingRowId, userId, userIdRank));
++userIdRank;
}
try {
context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY, operations);
} catch (RemoteException e) {
Log.e(Constants.TAG, "applyBatch failed!", e);
} catch (OperationApplicationException e) {
Log.e(Constants.TAG, "applyBatch failed!", e);
}
}
/**
* Saves PGPSecretKeyRing with its keys and userIds in DB
*
* @param context
* @param keyRing
* @return
* @throws IOException
* @throws GeneralException
*/
@SuppressWarnings("unchecked")
public static void saveKeyRing(Context context, PGPSecretKeyRing keyRing) throws IOException {
PGPSecretKey masterKey = keyRing.getSecretKey();
long masterKeyId = masterKey.getKeyID();
// delete old version of this keyRing, which also deletes all keys and userIds on cascade
Uri deleteUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
try {
context.getContentResolver().delete(deleteUri, null, null);
} catch (UnsupportedOperationException e) {
Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e);
}
ContentValues values = new ContentValues();
values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
// insert new version of this keyRing
Uri uri = KeyRings.buildSecretKeyRingsUri();
Uri insertedUri = context.getContentResolver().insert(uri, values);
long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment());
// save all keys and userIds included in keyRing object in database
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
int rank = 0;
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
operations.add(buildSecretKeyOperations(context, keyRingRowId, key, rank));
++rank;
}
int userIdRank = 0;
for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
operations.add(buildSecretUserIdOperations(context, keyRingRowId, userId, userIdRank));
++userIdRank;
}
try {
context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY, operations);
} catch (RemoteException e) {
Log.e(Constants.TAG, "applyBatch failed!", e);
} catch (OperationApplicationException e) {
Log.e(Constants.TAG, "applyBatch failed!", e);
}
}
/**
* Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing
*
* @param context
* @param keyRingRowId
* @param key
* @param rank
* @return
* @throws IOException
*/
private static ContentProviderOperation buildPublicKeyOperations(Context context,
long keyRingRowId, PGPPublicKey key, int rank) throws IOException {
ContentValues values = new ContentValues();
values.put(Keys.KEY_ID, key.getKeyID());
values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
values.put(Keys.ALGORITHM, key.getAlgorithm());
values.put(Keys.KEY_SIZE, key.getBitStrength());
values.put(Keys.CAN_SIGN, PgpKeyHelper.isSigningKey(key));
values.put(Keys.CAN_ENCRYPT, PgpKeyHelper.isEncryptionKey(key));
values.put(Keys.IS_REVOKED, key.isRevoked());
values.put(Keys.CREATION, PgpKeyHelper.getCreationDate(key).getTime() / 1000);
Date expiryDate = PgpKeyHelper.getExpiryDate(key);
if (expiryDate != null) {
values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
}
values.put(Keys.KEY_RING_ROW_ID, keyRingRowId);
values.put(Keys.KEY_DATA, key.getEncoded());
values.put(Keys.RANK, rank);
Uri uri = Keys.buildPublicKeysUri(Long.toString(keyRingRowId));
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
/**
* Build ContentProviderOperation to add PublicUserIds to database corresponding to a keyRing
*
* @param context
* @param keyRingRowId
* @param key
* @param rank
* @return
* @throws IOException
*/
private static ContentProviderOperation buildPublicUserIdOperations(Context context,
long keyRingRowId, String userId, int rank) {
ContentValues values = new ContentValues();
values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId);
values.put(UserIds.USER_ID, userId);
values.put(UserIds.RANK, rank);
Uri uri = UserIds.buildPublicUserIdsUri(Long.toString(keyRingRowId));
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
/**
* Build ContentProviderOperation to add PGPSecretKey to database corresponding to a keyRing
*
* @param context
* @param keyRingRowId
* @param key
* @param rank
* @return
* @throws IOException
*/
private static ContentProviderOperation buildSecretKeyOperations(Context context,
long keyRingRowId, PGPSecretKey key, int rank) throws IOException {
ContentValues values = new ContentValues();
boolean has_private = true;
if (key.isMasterKey()) {
if (PgpKeyHelper.isSecretKeyPrivateEmpty(key)) {
has_private = false;
}
}
values.put(Keys.KEY_ID, key.getKeyID());
values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
values.put(Keys.ALGORITHM, key.getPublicKey().getAlgorithm());
values.put(Keys.KEY_SIZE, key.getPublicKey().getBitStrength());
values.put(Keys.CAN_CERTIFY, (PgpKeyHelper.isCertificationKey(key) && has_private));
values.put(Keys.CAN_SIGN, (PgpKeyHelper.isSigningKey(key) && has_private));
values.put(Keys.CAN_ENCRYPT, PgpKeyHelper.isEncryptionKey(key));
values.put(Keys.IS_REVOKED, key.getPublicKey().isRevoked());
values.put(Keys.CREATION, PgpKeyHelper.getCreationDate(key).getTime() / 1000);
Date expiryDate = PgpKeyHelper.getExpiryDate(key);
if (expiryDate != null) {
values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
}
values.put(Keys.KEY_RING_ROW_ID, keyRingRowId);
values.put(Keys.KEY_DATA, key.getEncoded());
values.put(Keys.RANK, rank);
Uri uri = Keys.buildSecretKeysUri(Long.toString(keyRingRowId));
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
/**
* Build ContentProviderOperation to add SecretUserIds to database corresponding to a keyRing
*
* @param context
* @param keyRingRowId
* @param key
* @param rank
* @return
* @throws IOException
*/
private static ContentProviderOperation buildSecretUserIdOperations(Context context,
long keyRingRowId, String userId, int rank) {
ContentValues values = new ContentValues();
values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId);
values.put(UserIds.USER_ID, userId);
values.put(UserIds.RANK, rank);
Uri uri = UserIds.buildSecretUserIdsUri(Long.toString(keyRingRowId));
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
/**
* Private helper method
*
* @param context
* @param queryUri
* @return
*/
private static ArrayList<Long> getKeyRingsMasterKeyIds(Context context, Uri queryUri) {
Cursor cursor = context.getContentResolver().query(queryUri,
new String[] { KeyRings.MASTER_KEY_ID }, null, null, null);
ArrayList<Long> masterKeyIds = new ArrayList<Long>();
if (cursor != null) {
int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
if (cursor.moveToFirst()) {
do {
masterKeyIds.add(cursor.getLong(masterKeyIdCol));
} while (cursor.moveToNext());
}
}
if (cursor != null) {
cursor.close();
}
return masterKeyIds;
}
/**
* Retrieves ids of all SecretKeyRings
*
* @param context
* @return
*/
public static ArrayList<Long> getSecretKeyRingsMasterKeyIds(Context context) {
Uri queryUri = KeyRings.buildSecretKeyRingsUri();
return getKeyRingsMasterKeyIds(context, queryUri);
}
/**
* Retrieves ids of all PublicKeyRings
*
* @param context
* @return
*/
public static ArrayList<Long> getPublicKeyRingsMasterKeyIds(Context context) {
Uri queryUri = KeyRings.buildPublicKeyRingsUri();
return getKeyRingsMasterKeyIds(context, queryUri);
}
public static void deletePublicKeyRing(Context context, long rowId) {
ContentResolver cr = context.getContentResolver();
cr.delete(KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)), null, null);
}
public static void deleteSecretKeyRing(Context context, long rowId) {
ContentResolver cr = context.getContentResolver();
cr.delete(KeyRings.buildSecretKeyRingsUri(Long.toString(rowId)), null, null);
}
/**
* Get master key id of keyring by its row id
*
* @param context
* @param keyRingRowId
* @return
*/
public static long getPublicMasterKeyId(Context context, long keyRingRowId) {
Uri queryUri = KeyRings.buildPublicKeyRingsUri(String.valueOf(keyRingRowId));
return getMasterKeyId(context, queryUri);
}
/**
* Get empty status of master key of keyring by its row id
*
* @param context
* @param keyRingRowId
* @return
*/
public static boolean getSecretMasterKeyCanSign(Context context, long keyRingRowId) {
Uri queryUri = KeyRings.buildSecretKeyRingsUri(String.valueOf(keyRingRowId));
return getMasterKeyCanSign(context, queryUri, keyRingRowId);
}
/**
* Private helper method to get master key private empty status of keyring by its row id
*
* @param context
* @param queryUri
* @param keyRingRowId
* @return
*/
private static boolean getMasterKeyCanSign(Context context, Uri queryUri, long keyRingRowId) {
String[] projection = new String[] {
KeyRings.MASTER_KEY_ID,
"(SELECT COUNT(sign_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ " AS sign_keys WHERE sign_keys." + Keys.KEY_RING_ROW_ID + " = "
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ " AND sign_keys." + Keys.CAN_SIGN + " = '1' AND " + Keys.IS_MASTER_KEY
+ " = 1) AS sign", };
ContentResolver cr = context.getContentResolver();
Cursor cursor = cr.query(queryUri, projection, null, null, null);
long masterKeyId = -1;
if (cursor != null && cursor.moveToFirst()) {
int masterKeyIdCol = cursor.getColumnIndex("sign");
masterKeyId = cursor.getLong(masterKeyIdCol);
}
if (cursor != null) {
cursor.close();
}
return (masterKeyId > 0);
}
/**
* Get master key id of keyring by its row id
*
* @param context
* @param keyRingRowId
* @return
*/
public static long getSecretMasterKeyId(Context context, long keyRingRowId) {
Uri queryUri = KeyRings.buildSecretKeyRingsUri(String.valueOf(keyRingRowId));
return getMasterKeyId(context, queryUri);
}
/**
* Private helper method to get master key id of keyring by its row id
*
* @param context
* @param queryUri
* @param keyRingRowId
* @return
*/
public static long getMasterKeyId(Context context, Uri queryUri) {
String[] projection = new String[] { KeyRings.MASTER_KEY_ID };
ContentResolver cr = context.getContentResolver();
Cursor cursor = cr.query(queryUri, projection, null, null, null);
long masterKeyId = -1;
if (cursor != null && cursor.moveToFirst()) {
int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
masterKeyId = cursor.getLong(masterKeyIdCol);
}
if (cursor != null) {
cursor.close();
}
return masterKeyId;
}
public static ArrayList<String> getPublicKeyRingsAsArmoredString(Context context,
long[] masterKeyIds) {
return getKeyRingsAsArmoredString(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds);
}
public static ArrayList<String> getSecretKeyRingsAsArmoredString(Context context,
long[] masterKeyIds) {
return getKeyRingsAsArmoredString(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds);
}
public static ArrayList<String> getKeyRingsAsArmoredString(Context context, Uri uri,
long[] masterKeyIds) {
ArrayList<String> output = new ArrayList<String>();
if (masterKeyIds != null && masterKeyIds.length > 0) {
Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds);
if (cursor != null) {
int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
if (cursor.moveToFirst()) {
do {
Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol));
// get actual keyring data blob and write it to ByteArrayOutputStream
try {
Object keyRing = null;
byte[] data = cursor.getBlob(dataCol);
if (data != null) {
keyRing = PgpConversionHelper.BytesToPGPKeyRing(data);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ArmoredOutputStream aos = new ArmoredOutputStream(bos);
aos.setHeader("Version", PgpHelper.getFullVersion(context));
if (keyRing instanceof PGPSecretKeyRing) {
aos.write(((PGPSecretKeyRing) keyRing).getEncoded());
} else if (keyRing instanceof PGPPublicKeyRing) {
aos.write(((PGPPublicKeyRing) keyRing).getEncoded());
}
aos.close();
String armoredKey = bos.toString("UTF-8");
Log.d(Constants.TAG, "armouredKey:" + armoredKey);
output.add(armoredKey);
} catch (IOException e) {
Log.e(Constants.TAG, "IOException", e);
}
} while (cursor.moveToNext());
}
}
if (cursor != null) {
cursor.close();
}
} else {
Log.e(Constants.TAG, "No master keys given!");
}
if (output.size() > 0) {
return output;
} else {
return null;
}
}
public static byte[] getPublicKeyRingsAsByteArray(Context context, long[] masterKeyIds) {
return getKeyRingsAsByteArray(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds);
}
public static byte[] getSecretKeyRingsAsByteArray(Context context, long[] masterKeyIds) {
return getKeyRingsAsByteArray(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds);
}
public static byte[] getKeyRingsAsByteArray(Context context, Uri uri, long[] masterKeyIds) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (masterKeyIds != null && masterKeyIds.length > 0) {
Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds);
if (cursor != null) {
int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
if (cursor.moveToFirst()) {
do {
Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol));
// get actual keyring data blob and write it to ByteArrayOutputStream
try {
bos.write(cursor.getBlob(dataCol));
} catch (IOException e) {
Log.e(Constants.TAG, "IOException", e);
}
} while (cursor.moveToNext());
}
}
if (cursor != null) {
cursor.close();
}
} else {
Log.e(Constants.TAG, "No master keys given!");
}
return bos.toByteArray();
}
private static Cursor getCursorWithSelectedKeyringMasterKeyIds(Context context, Uri baseUri,
long[] masterKeyIds) {
Cursor cursor = null;
if (masterKeyIds != null && masterKeyIds.length > 0) {
String inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN (";
for (int i = 0; i < masterKeyIds.length; ++i) {
if (i != 0) {
inMasterKeyList += ", ";
}
inMasterKeyList += DatabaseUtils.sqlEscapeString("" + masterKeyIds[i]);
}
inMasterKeyList += ")";
cursor = context.getContentResolver().query(baseUri,
new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.KEY_RING_DATA },
inMasterKeyList, null, null);
}
return cursor;
}
public static ArrayList<String> getRegisteredApiApps(Context context) {
Cursor cursor = context.getContentResolver().query(ApiApps.CONTENT_URI, null, null, null,
null);
ArrayList<String> packageNames = new ArrayList<String>();
if (cursor != null) {
int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
if (cursor.moveToFirst()) {
do {
packageNames.add(cursor.getString(packageNameCol));
} while (cursor.moveToNext());
}
}
if (cursor != null) {
cursor.close();
}
return packageNames;
}
private static ContentValues contentValueForApiApps(AppSettings appSettings) {
ContentValues values = new ContentValues();
values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
values.put(ApiApps.PACKAGE_SIGNATURE, appSettings.getPackageSignature());
values.put(ApiApps.KEY_ID, appSettings.getKeyId());
values.put(ApiApps.COMPRESSION, appSettings.getCompression());
values.put(ApiApps.ENCRYPTION_ALGORITHM, appSettings.getEncryptionAlgorithm());
values.put(ApiApps.HASH_ALORITHM, appSettings.getHashAlgorithm());
return values;
}
public static void insertApiApp(Context context, AppSettings appSettings) {
context.getContentResolver().insert(ApiApps.CONTENT_URI,
contentValueForApiApps(appSettings));
}
public static void updateApiApp(Context context, AppSettings appSettings, Uri uri) {
if (context.getContentResolver().update(uri, contentValueForApiApps(appSettings), null,
null) <= 0) {
throw new RuntimeException();
}
}
public static AppSettings getApiAppSettings(Context context, Uri uri) {
AppSettings settings = null;
Cursor cur = context.getContentResolver().query(uri, null, null, null, null);
if (cur != null && cur.moveToFirst()) {
settings = new AppSettings();
settings.setPackageName(cur.getString(cur
.getColumnIndex(KeychainContract.ApiApps.PACKAGE_NAME)));
settings.setPackageSignature(cur.getBlob(cur
.getColumnIndex(KeychainContract.ApiApps.PACKAGE_SIGNATURE)));
settings.setKeyId(cur.getLong(cur.getColumnIndex(KeychainContract.ApiApps.KEY_ID)));
settings.setCompression(cur.getInt(cur
.getColumnIndexOrThrow(KeychainContract.ApiApps.COMPRESSION)));
settings.setHashAlgorithm(cur.getInt(cur
.getColumnIndexOrThrow(KeychainContract.ApiApps.HASH_ALORITHM)));
settings.setEncryptionAlgorithm(cur.getInt(cur
.getColumnIndexOrThrow(KeychainContract.ApiApps.ENCRYPTION_ALGORITHM)));
}
return settings;
}
public static byte[] getApiAppSignature(Context context, String packageName) {
Uri queryUri = KeychainContract.ApiApps.buildByPackageNameUri(packageName);
String[] projection = new String[] { ApiApps.PACKAGE_SIGNATURE };
ContentResolver cr = context.getContentResolver();
Cursor cursor = cr.query(queryUri, projection, null, null, null);
byte[] signature = null;
if (cursor != null && cursor.moveToFirst()) {
int signatureCol = 0;
signature = cursor.getBlob(signatureCol);
}
if (cursor != null) {
cursor.close();
}
return signature;
}
}

View File

@@ -0,0 +1,869 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.FileHelper;
import org.sufficientlysecure.keychain.helper.OtherHelper;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpImportExport;
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
import org.sufficientlysecure.keychain.pgp.PgpOperation;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract.DataStream;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import org.sufficientlysecure.keychain.util.HkpKeyServer;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
/**
* This Service contains all important long lasting operations for APG. It receives Intents with
* data from the activities or other apps, queues these intents, executes them, and stops itself
* after doing them.
*/
public class KeychainIntentService extends IntentService implements ProgressDialogUpdater {
/* extras that can be given by intent */
public static final String EXTRA_MESSENGER = "messenger";
public static final String EXTRA_DATA = "data";
/* possible actions */
public static final String ACTION_ENCRYPT_SIGN = Constants.INTENT_PREFIX + "ENCRYPT_SIGN";
public static final String ACTION_DECRYPT_VERIFY = Constants.INTENT_PREFIX + "DECRYPT_VERIFY";
public static final String ACTION_SAVE_KEYRING = Constants.INTENT_PREFIX + "SAVE_KEYRING";
public static final String ACTION_GENERATE_KEY = Constants.INTENT_PREFIX + "GENERATE_KEY";
public static final String ACTION_GENERATE_DEFAULT_RSA_KEYS = Constants.INTENT_PREFIX
+ "GENERATE_DEFAULT_RSA_KEYS";
public static final String ACTION_DELETE_FILE_SECURELY = Constants.INTENT_PREFIX
+ "DELETE_FILE_SECURELY";
public static final String ACTION_IMPORT_KEYRING = Constants.INTENT_PREFIX + "IMPORT_KEYRING";
public static final String ACTION_EXPORT_KEYRING = Constants.INTENT_PREFIX + "EXPORT_KEYRING";
public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING";
public static final String ACTION_QUERY_KEYRING = Constants.INTENT_PREFIX + "QUERY_KEYRING";
public static final String ACTION_SIGN_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING";
/* keys for data bundle */
// encrypt, decrypt, import export
public static final String TARGET = "target";
// possible targets:
public static final int TARGET_BYTES = 1;
public static final int TARGET_FILE = 2;
public static final int TARGET_STREAM = 3;
// encrypt
public static final String ENCRYPT_SECRET_KEY_ID = "secret_key_id";
public static final String ENCRYPT_USE_ASCII_ARMOR = "use_ascii_armor";
public static final String ENCRYPT_ENCRYPTION_KEYS_IDS = "encryption_keys_ids";
public static final String ENCRYPT_COMPRESSION_ID = "compression_id";
public static final String ENCRYPT_GENERATE_SIGNATURE = "generate_signature";
public static final String ENCRYPT_SIGN_ONLY = "sign_only";
public static final String ENCRYPT_MESSAGE_BYTES = "message_bytes";
public static final String ENCRYPT_INPUT_FILE = "input_file";
public static final String ENCRYPT_OUTPUT_FILE = "output_file";
public static final String ENCRYPT_PROVIDER_URI = "provider_uri";
// decrypt/verify
public static final String DECRYPT_SIGNED_ONLY = "signed_only";
public static final String DECRYPT_RETURN_BYTES = "return_binary";
public static final String DECRYPT_CIPHERTEXT_BYTES = "ciphertext_bytes";
public static final String DECRYPT_ASSUME_SYMMETRIC = "assume_symmetric";
public static final String DECRYPT_LOOKUP_UNKNOWN_KEY = "lookup_unknownKey";
// save keyring
public static final String SAVE_KEYRING_NEW_PASSPHRASE = "new_passphrase";
public static final String SAVE_KEYRING_CURRENT_PASSPHRASE = "current_passphrase";
public static final String SAVE_KEYRING_USER_IDS = "user_ids";
public static final String SAVE_KEYRING_KEYS = "keys";
public static final String SAVE_KEYRING_KEYS_USAGES = "keys_usages";
public static final String SAVE_KEYRING_MASTER_KEY_ID = "master_key_id";
public static final String SAVE_KEYRING_CAN_SIGN = "can_sign";
// generate key
public static final String GENERATE_KEY_ALGORITHM = "algorithm";
public static final String GENERATE_KEY_KEY_SIZE = "key_size";
public static final String GENERATE_KEY_SYMMETRIC_PASSPHRASE = "passphrase";
public static final String GENERATE_KEY_MASTER_KEY = "master_key";
// delete file securely
public static final String DELETE_FILE = "deleteFile";
// import key
public static final String IMPORT_INPUT_STREAM = "import_input_stream";
public static final String IMPORT_FILENAME = "import_filename";
public static final String IMPORT_BYTES = "import_bytes";
public static final String IMPORT_KEY_LIST = "import_key_list";
// export key
public static final String EXPORT_OUTPUT_STREAM = "export_output_stream";
public static final String EXPORT_FILENAME = "export_filename";
public static final String EXPORT_KEY_TYPE = "export_key_type";
public static final String EXPORT_ALL = "export_all";
public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "export_key_ring_id";
// upload key
public static final String UPLOAD_KEY_SERVER = "upload_key_server";
public static final String UPLOAD_KEY_KEYRING_ROW_ID = "upload_key_ring_id";
// query key
public static final String QUERY_KEY_SERVER = "query_key_server";
public static final String QUERY_KEY_TYPE = "query_key_type";
public static final String QUERY_KEY_STRING = "query_key_string";
public static final String QUERY_KEY_ID = "query_key_id";
// sign key
public static final String SIGN_KEY_MASTER_KEY_ID = "sign_key_master_key_id";
public static final String SIGN_KEY_PUB_KEY_ID = "sign_key_pub_key_id";
/*
* possible data keys as result send over messenger
*/
// keys
public static final String RESULT_NEW_KEY = "new_key";
public static final String RESULT_NEW_KEY2 = "new_key2";
// encrypt
public static final String RESULT_SIGNATURE_BYTES = "signature_data";
public static final String RESULT_SIGNATURE_STRING = "signature_text";
public static final String RESULT_ENCRYPTED_STRING = "encrypted_message";
public static final String RESULT_ENCRYPTED_BYTES = "encrypted_data";
public static final String RESULT_URI = "result_uri";
// decrypt/verify
public static final String RESULT_DECRYPTED_STRING = "decrypted_message";
public static final String RESULT_DECRYPTED_BYTES = "decrypted_data";
public static final String RESULT_SIGNATURE = "signature";
public static final String RESULT_SIGNATURE_KEY_ID = "signature_key_id";
public static final String RESULT_SIGNATURE_USER_ID = "signature_user_id";
public static final String RESULT_SIGNATURE_SUCCESS = "signature_success";
public static final String RESULT_SIGNATURE_UNKNOWN = "signature_unknown";
public static final String RESULT_SIGNATURE_LOOKUP_KEY = "lookup_key";
// import
public static final String RESULT_IMPORT_ADDED = "added";
public static final String RESULT_IMPORT_UPDATED = "updated";
public static final String RESULT_IMPORT_BAD = "bad";
// export
public static final String RESULT_EXPORT = "exported";
// query
public static final String RESULT_QUERY_KEY_DATA = "query_key_data";
public static final String RESULT_QUERY_KEY_SEARCH_RESULT = "query_key_search_result";
Messenger mMessenger;
public KeychainIntentService() {
super("ApgService");
}
/**
* The IntentService calls this method from the default worker thread with the intent that
* started the service. When this method returns, IntentService stops the service, as
* appropriate.
*/
@Override
protected void onHandleIntent(Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) {
Log.e(Constants.TAG, "Extras bundle is null!");
return;
}
if (!(extras.containsKey(EXTRA_MESSENGER) || extras.containsKey(EXTRA_DATA) || (intent
.getAction() == null))) {
Log.e(Constants.TAG,
"Extra bundle must contain a messenger, a data bundle, and an action!");
return;
}
mMessenger = (Messenger) extras.get(EXTRA_MESSENGER);
Bundle data = extras.getBundle(EXTRA_DATA);
OtherHelper.logDebugBundle(data, "EXTRA_DATA");
String action = intent.getAction();
// execute action from extra bundle
if (ACTION_ENCRYPT_SIGN.equals(action)) {
try {
/* Input */
int target = data.getInt(TARGET);
long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
String encryptionPassphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
boolean useAsciiArmor = data.getBoolean(ENCRYPT_USE_ASCII_ARMOR);
long encryptionKeyIds[] = data.getLongArray(ENCRYPT_ENCRYPTION_KEYS_IDS);
int compressionId = data.getInt(ENCRYPT_COMPRESSION_ID);
boolean generateSignature = data.getBoolean(ENCRYPT_GENERATE_SIGNATURE);
boolean signOnly = data.getBoolean(ENCRYPT_SIGN_ONLY);
InputStream inStream = null;
long inLength = -1;
InputData inputData = null;
OutputStream outStream = null;
String streamFilename = null;
switch (target) {
case TARGET_BYTES: /* encrypting bytes directly */
byte[] bytes = data.getByteArray(ENCRYPT_MESSAGE_BYTES);
inStream = new ByteArrayInputStream(bytes);
inLength = bytes.length;
inputData = new InputData(inStream, inLength);
outStream = new ByteArrayOutputStream();
break;
case TARGET_FILE: /* encrypting file */
String inputFile = data.getString(ENCRYPT_INPUT_FILE);
String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
// check if storage is ready
if (!FileHelper.isStorageMounted(inputFile)
|| !FileHelper.isStorageMounted(outputFile)) {
throw new PgpGeneralException(
getString(R.string.error_external_storage_not_ready));
}
inStream = new FileInputStream(inputFile);
File file = new File(inputFile);
inLength = file.length();
inputData = new InputData(inStream, inLength);
outStream = new FileOutputStream(outputFile);
break;
case TARGET_STREAM: /* Encrypting stream from content uri */
Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
// InputStream
InputStream in = getContentResolver().openInputStream(providerUri);
inLength = PgpHelper.getLengthOfStream(in);
inputData = new InputData(in, inLength);
// OutputStream
try {
while (true) {
streamFilename = PgpHelper.generateRandomFilename(32);
if (streamFilename == null) {
throw new PgpGeneralException("couldn't generate random file name");
}
openFileInput(streamFilename).close();
}
} catch (FileNotFoundException e) {
// found a name that isn't used yet
}
outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
break;
default:
throw new PgpGeneralException("No target choosen!");
}
/* Operation */
PgpOperation operation = new PgpOperation(this, this, inputData, outStream);
if (generateSignature) {
Log.d(Constants.TAG, "generating signature...");
operation.generateSignature(useAsciiArmor, false, secretKeyId,
PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
Preferences.getPreferences(this).getDefaultHashAlgorithm(), Preferences
.getPreferences(this).getForceV3Signatures());
} else if (signOnly) {
Log.d(Constants.TAG, "sign only...");
operation.signText(secretKeyId, PassphraseCacheService.getCachedPassphrase(
this, secretKeyId), Preferences.getPreferences(this)
.getDefaultHashAlgorithm(), Preferences.getPreferences(this)
.getForceV3Signatures());
} else {
Log.d(Constants.TAG, "encrypt...");
operation.signAndEncrypt(useAsciiArmor, compressionId, encryptionKeyIds,
encryptionPassphrase, Preferences.getPreferences(this)
.getDefaultEncryptionAlgorithm(), secretKeyId, Preferences
.getPreferences(this).getDefaultHashAlgorithm(), Preferences
.getPreferences(this).getForceV3Signatures(),
PassphraseCacheService.getCachedPassphrase(this, secretKeyId));
}
outStream.close();
/* Output */
Bundle resultData = new Bundle();
switch (target) {
case TARGET_BYTES:
if (useAsciiArmor) {
String output = new String(
((ByteArrayOutputStream) outStream).toByteArray());
if (generateSignature) {
resultData.putString(RESULT_SIGNATURE_STRING, output);
} else {
resultData.putString(RESULT_ENCRYPTED_STRING, output);
}
} else {
byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
if (generateSignature) {
resultData.putByteArray(RESULT_SIGNATURE_BYTES, output);
} else {
resultData.putByteArray(RESULT_ENCRYPTED_BYTES, output);
}
}
break;
case TARGET_FILE:
// nothing, file was written, just send okay
break;
case TARGET_STREAM:
String uri = DataStream.buildDataStreamUri(streamFilename).toString();
resultData.putString(RESULT_URI, uri);
break;
}
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_DECRYPT_VERIFY.equals(action)) {
try {
/* Input */
int target = data.getInt(TARGET);
long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
byte[] bytes = data.getByteArray(DECRYPT_CIPHERTEXT_BYTES);
boolean signedOnly = data.getBoolean(DECRYPT_SIGNED_ONLY);
boolean returnBytes = data.getBoolean(DECRYPT_RETURN_BYTES);
boolean assumeSymmetricEncryption = data.getBoolean(DECRYPT_ASSUME_SYMMETRIC);
boolean lookupUnknownKey = data.getBoolean(DECRYPT_LOOKUP_UNKNOWN_KEY);
InputStream inStream = null;
long inLength = -1;
InputData inputData = null;
OutputStream outStream = null;
String streamFilename = null;
switch (target) {
case TARGET_BYTES: /* decrypting bytes directly */
inStream = new ByteArrayInputStream(bytes);
inLength = bytes.length;
inputData = new InputData(inStream, inLength);
outStream = new ByteArrayOutputStream();
break;
case TARGET_FILE: /* decrypting file */
String inputFile = data.getString(ENCRYPT_INPUT_FILE);
String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
// check if storage is ready
if (!FileHelper.isStorageMounted(inputFile)
|| !FileHelper.isStorageMounted(outputFile)) {
throw new PgpGeneralException(
getString(R.string.error_external_storage_not_ready));
}
// InputStream
inLength = -1;
inStream = new FileInputStream(inputFile);
File file = new File(inputFile);
inLength = file.length();
inputData = new InputData(inStream, inLength);
// OutputStream
outStream = new FileOutputStream(outputFile);
break;
case TARGET_STREAM: /* decrypting stream from content uri */
Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
// InputStream
InputStream in = getContentResolver().openInputStream(providerUri);
inLength = PgpHelper.getLengthOfStream(in);
inputData = new InputData(in, inLength);
// OutputStream
try {
while (true) {
streamFilename = PgpHelper.generateRandomFilename(32);
if (streamFilename == null) {
throw new PgpGeneralException("couldn't generate random file name");
}
openFileInput(streamFilename).close();
}
} catch (FileNotFoundException e) {
// found a name that isn't used yet
}
outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
break;
default:
throw new PgpGeneralException("No target choosen!");
}
/* Operation */
Bundle resultData = new Bundle();
// verifyText and decrypt returning additional resultData values for the
// verification of signatures
PgpOperation operation = new PgpOperation(this, this, inputData, outStream);
if (signedOnly) {
resultData = operation.verifyText(lookupUnknownKey);
} else {
resultData = operation.decryptAndVerify(
PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
assumeSymmetricEncryption);
}
outStream.close();
/* Output */
switch (target) {
case TARGET_BYTES:
if (returnBytes) {
byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
resultData.putByteArray(RESULT_DECRYPTED_BYTES, output);
} else {
String output = new String(
((ByteArrayOutputStream) outStream).toByteArray());
resultData.putString(RESULT_DECRYPTED_STRING, output);
}
break;
case TARGET_FILE:
// nothing, file was written, just send okay and verification bundle
break;
case TARGET_STREAM:
String uri = DataStream.buildDataStreamUri(streamFilename).toString();
resultData.putString(RESULT_URI, uri);
break;
}
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_SAVE_KEYRING.equals(action)) {
try {
/* Input */
String oldPassPhrase = data.getString(SAVE_KEYRING_CURRENT_PASSPHRASE);
String newPassPhrase = data.getString(SAVE_KEYRING_NEW_PASSPHRASE);
boolean canSign = true;
if (data.containsKey(SAVE_KEYRING_CAN_SIGN)) {
canSign = data.getBoolean(SAVE_KEYRING_CAN_SIGN);
}
if (newPassPhrase == null) {
newPassPhrase = oldPassPhrase;
}
ArrayList<String> userIds = data.getStringArrayList(SAVE_KEYRING_USER_IDS);
ArrayList<PGPSecretKey> keys = PgpConversionHelper.BytesToPGPSecretKeyList(data
.getByteArray(SAVE_KEYRING_KEYS));
ArrayList<Integer> keysUsages = data.getIntegerArrayList(SAVE_KEYRING_KEYS_USAGES);
long masterKeyId = data.getLong(SAVE_KEYRING_MASTER_KEY_ID);
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
/* Operation */
if (!canSign) {
keyOperations.changeSecretKeyPassphrase(
ProviderHelper.getPGPSecretKeyRingByKeyId(this, masterKeyId),
oldPassPhrase, newPassPhrase);
} else {
keyOperations.buildSecretKey(userIds, keys, keysUsages, masterKeyId,
oldPassPhrase, newPassPhrase);
}
PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassPhrase);
/* Output */
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_GENERATE_KEY.equals(action)) {
try {
/* Input */
int algorithm = data.getInt(GENERATE_KEY_ALGORITHM);
String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
int keysize = data.getInt(GENERATE_KEY_KEY_SIZE);
PGPSecretKey masterKey = null;
if (data.containsKey(GENERATE_KEY_MASTER_KEY)) {
masterKey = PgpConversionHelper.BytesToPGPSecretKey(data
.getByteArray(GENERATE_KEY_MASTER_KEY));
}
/* Operation */
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
PGPSecretKeyRing newKeyRing = keyOperations.createKey(algorithm, keysize,
passphrase, masterKey);
/* Output */
Bundle resultData = new Bundle();
resultData.putByteArray(RESULT_NEW_KEY,
PgpConversionHelper.PGPSecretKeyRingToBytes(newKeyRing));
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_GENERATE_DEFAULT_RSA_KEYS.equals(action)) {
// generate one RSA 4096 key for signing and one subkey for encrypting!
try {
/* Input */
String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
/* Operation */
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
PGPSecretKeyRing masterKeyRing = keyOperations.createKey(Id.choice.algorithm.rsa,
4096, passphrase, null);
PGPSecretKeyRing subKeyRing = keyOperations.createKey(Id.choice.algorithm.rsa,
4096, passphrase, masterKeyRing.getSecretKey());
/* Output */
Bundle resultData = new Bundle();
resultData.putByteArray(RESULT_NEW_KEY,
PgpConversionHelper.PGPSecretKeyRingToBytes(masterKeyRing));
resultData.putByteArray(RESULT_NEW_KEY2,
PgpConversionHelper.PGPSecretKeyRingToBytes(subKeyRing));
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_DELETE_FILE_SECURELY.equals(action)) {
try {
/* Input */
String deleteFile = data.getString(DELETE_FILE);
/* Operation */
try {
PgpHelper.deleteFileSecurely(this, this, new File(deleteFile));
} catch (FileNotFoundException e) {
throw new PgpGeneralException(
getString(R.string.error_file_not_found, deleteFile));
} catch (IOException e) {
throw new PgpGeneralException(getString(R.string.error_file_delete_failed,
deleteFile));
}
/* Output */
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_IMPORT_KEYRING.equals(action)) {
try {
/* Input */
int target = data.getInt(TARGET);
/* Operation */
InputStream inStream = null;
long inLength = -1;
InputData inputData = null;
switch (target) {
case TARGET_BYTES: /* import key from bytes directly */
byte[] bytes = data.getByteArray(IMPORT_BYTES);
inStream = new ByteArrayInputStream(bytes);
inLength = bytes.length;
inputData = new InputData(inStream, inLength);
break;
case TARGET_FILE: /* import key from file */
String inputFile = data.getString(IMPORT_FILENAME);
inStream = new FileInputStream(inputFile);
File file = new File(inputFile);
inLength = file.length();
inputData = new InputData(inStream, inLength);
break;
case TARGET_STREAM:
// TODO: not implemented
break;
}
Bundle resultData = new Bundle();
ArrayList<Long> keyIds = (ArrayList<Long>) data.getSerializable(IMPORT_KEY_LIST);
PgpImportExport pgpImportExport = new PgpImportExport(this, this);
resultData = pgpImportExport.importKeyRings(inputData, keyIds);
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_EXPORT_KEYRING.equals(action)) {
try {
/* Input */
int keyType = Id.type.public_key;
if (data.containsKey(EXPORT_KEY_TYPE)) {
keyType = data.getInt(EXPORT_KEY_TYPE);
}
String outputFile = data.getString(EXPORT_FILENAME);
boolean exportAll = data.getBoolean(EXPORT_ALL);
long keyRingMasterKeyId = -1;
if (!exportAll) {
keyRingMasterKeyId = data.getLong(EXPORT_KEY_RING_MASTER_KEY_ID);
}
/* Operation */
// check if storage is ready
if (!FileHelper.isStorageMounted(outputFile)) {
throw new PgpGeneralException(getString(R.string.error_external_storage_not_ready));
}
// OutputStream
FileOutputStream outStream = new FileOutputStream(outputFile);
ArrayList<Long> keyRingMasterKeyIds = new ArrayList<Long>();
if (exportAll) {
// get all key ring row ids based on export type
if (keyType == Id.type.public_key) {
keyRingMasterKeyIds = ProviderHelper.getPublicKeyRingsMasterKeyIds(this);
} else {
keyRingMasterKeyIds = ProviderHelper.getSecretKeyRingsMasterKeyIds(this);
}
} else {
keyRingMasterKeyIds.add(keyRingMasterKeyId);
}
Bundle resultData = new Bundle();
PgpImportExport pgpImportExport = new PgpImportExport(this, this);
resultData = pgpImportExport
.exportKeyRings(keyRingMasterKeyIds, keyType, outStream);
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_UPLOAD_KEYRING.equals(action)) {
try {
/* Input */
int keyRingRowId = data.getInt(UPLOAD_KEY_KEYRING_ROW_ID);
String keyServer = data.getString(UPLOAD_KEY_SERVER);
/* Operation */
HkpKeyServer server = new HkpKeyServer(keyServer);
PGPPublicKeyRing keyring = ProviderHelper.getPGPPublicKeyRingByRowId(this,
keyRingRowId);
if (keyring != null) {
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
boolean uploaded = pgpImportExport.uploadKeyRingToServer(server,
(PGPPublicKeyRing) keyring);
if (!uploaded) {
throw new PgpGeneralException("Unable to export key to selected server");
}
}
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_QUERY_KEYRING.equals(action)) {
try {
/* Input */
int queryType = data.getInt(QUERY_KEY_TYPE);
String keyServer = data.getString(QUERY_KEY_SERVER);
String queryString = data.getString(QUERY_KEY_STRING);
long keyId = data.getLong(QUERY_KEY_ID);
/* Operation */
Bundle resultData = new Bundle();
HkpKeyServer server = new HkpKeyServer(keyServer);
if (queryType == Id.keyserver.search) {
ArrayList<KeyInfo> searchResult = server.search(queryString);
resultData.putParcelableArrayList(RESULT_QUERY_KEY_SEARCH_RESULT, searchResult);
} else if (queryType == Id.keyserver.get) {
String keyData = server.get(keyId);
resultData.putString(RESULT_QUERY_KEY_DATA, keyData);
}
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_SIGN_KEYRING.equals(action)) {
try {
/* Input */
long masterKeyId = data.getLong(SIGN_KEY_MASTER_KEY_ID);
long pubKeyId = data.getLong(SIGN_KEY_PUB_KEY_ID);
/* Operation */
String signaturePassPhrase = PassphraseCacheService.getCachedPassphrase(this,
masterKeyId);
PgpKeyOperation keyOperation = new PgpKeyOperation(this, this);
PGPPublicKeyRing signedPubKeyRing = keyOperation.signKey(masterKeyId, pubKeyId,
signaturePassPhrase);
// store the signed key in our local cache
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
int retval = pgpImportExport.storeKeyRingInCache(signedPubKeyRing);
if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
throw new PgpGeneralException("Failed to store signed key in local cache");
}
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
}
}
private void sendErrorToHandler(Exception e) {
Log.e(Constants.TAG, "ApgService Exception: ", e);
e.printStackTrace();
Bundle data = new Bundle();
data.putString(KeychainIntentServiceHandler.DATA_ERROR, e.getMessage());
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_EXCEPTION, null, data);
}
private void sendMessageToHandler(Integer arg1, Integer arg2, Bundle data) {
Message msg = Message.obtain();
msg.arg1 = arg1;
if (arg2 != null) {
msg.arg2 = arg2;
}
if (data != null) {
msg.setData(data);
}
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
} catch (NullPointerException e) {
Log.w(Constants.TAG, "Messenger is null!", e);
}
}
private void sendMessageToHandler(Integer arg1, Bundle data) {
sendMessageToHandler(arg1, null, data);
}
private void sendMessageToHandler(Integer arg1) {
sendMessageToHandler(arg1, null, null);
}
/**
* Set progress of ProgressDialog by sending message to handler on UI thread
*/
public void setProgress(String message, int progress, int max) {
Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max="
+ max);
Bundle data = new Bundle();
if (message != null) {
data.putString(KeychainIntentServiceHandler.DATA_MESSAGE, message);
}
data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS, progress);
data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS_MAX, max);
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_UPDATE_PROGRESS, null, data);
}
public void setProgress(int resourceId, int progress, int max) {
setProgress(getString(resourceId), progress, max);
}
public void setProgress(int progress, int max) {
setProgress(null, progress, max);
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
import org.sufficientlysecure.keychain.R;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.FragmentActivity;
import android.widget.Toast;
public class KeychainIntentServiceHandler extends Handler {
// possible messages send from this service to handler on ui
public static final int MESSAGE_OKAY = 1;
public static final int MESSAGE_EXCEPTION = 2;
public static final int MESSAGE_UPDATE_PROGRESS = 3;
// possible data keys for messages
public static final String DATA_ERROR = "error";
public static final String DATA_PROGRESS = "progress";
public static final String DATA_PROGRESS_MAX = "max";
public static final String DATA_MESSAGE = "message";
public static final String DATA_MESSAGE_ID = "message_id";
Activity mActivity;
ProgressDialogFragment mProgressDialogFragment;
public KeychainIntentServiceHandler(Activity activity) {
this.mActivity = activity;
}
public KeychainIntentServiceHandler(Activity activity, ProgressDialogFragment progressDialogFragment) {
this.mActivity = activity;
this.mProgressDialogFragment = progressDialogFragment;
}
public KeychainIntentServiceHandler(Activity activity, int progressDialogMessageId, int progressDialogStyle) {
this.mActivity = activity;
this.mProgressDialogFragment = ProgressDialogFragment.newInstance(progressDialogMessageId,
progressDialogStyle);
}
public void showProgressDialog(FragmentActivity activity) {
mProgressDialogFragment.show(activity.getSupportFragmentManager(), "progressDialog");
}
@Override
public void handleMessage(Message message) {
Bundle data = message.getData();
switch (message.arg1) {
case MESSAGE_OKAY:
mProgressDialogFragment.dismiss();
break;
case MESSAGE_EXCEPTION:
mProgressDialogFragment.dismiss();
// show error from service
if (data.containsKey(DATA_ERROR)) {
Toast.makeText(mActivity,
mActivity.getString(R.string.error_message, data.getString(DATA_ERROR)),
Toast.LENGTH_SHORT).show();
}
break;
case MESSAGE_UPDATE_PROGRESS:
if (data.containsKey(DATA_PROGRESS) && data.containsKey(DATA_PROGRESS_MAX)) {
// update progress from service
if (data.containsKey(DATA_MESSAGE)) {
mProgressDialogFragment.setProgress(data.getString(DATA_MESSAGE),
data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
} else if (data.containsKey(DATA_MESSAGE_ID)) {
mProgressDialogFragment.setProgress(data.getInt(DATA_MESSAGE_ID),
data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
} else {
mProgressDialogFragment.setProgress(data.getInt(DATA_PROGRESS),
data.getInt(DATA_PROGRESS_MAX));
}
}
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,370 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import java.util.Date;
import java.util.HashMap;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
/**
* This service runs in its own process, but is available to all other processes as the main
* passphrase cache. Use the static methods addCachedPassphrase and getCachedPassphrase for
* convenience.
*
*/
public class PassphraseCacheService extends Service {
public static final String TAG = Constants.TAG + ": PassphraseCacheService";
public static final String ACTION_PASSPHRASE_CACHE_ADD = Constants.INTENT_PREFIX
+ "PASSPHRASE_CACHE_ADD";
public static final String ACTION_PASSPHRASE_CACHE_GET = Constants.INTENT_PREFIX
+ "PASSPHRASE_CACHE_GET";
public static final String BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE = Constants.INTENT_PREFIX
+ "PASSPHRASE_CACHE_BROADCAST";
public static final String EXTRA_TTL = "ttl";
public static final String EXTRA_KEY_ID = "key_id";
public static final String EXTRA_PASSPHRASE = "passphrase";
public static final String EXTRA_MESSENGER = "messenger";
private static final int REQUEST_ID = 0;
private static final long DEFAULT_TTL = 15;
private BroadcastReceiver mIntentReceiver;
private HashMap<Long, String> mPassphraseCache = new HashMap<Long, String>();
Context mContext;
/**
* This caches a new passphrase in memory by sending a new command to the service. An android
* service is only run once. Thus, when the service is already started, new commands just add
* new events to the alarm manager for new passphrases to let them timeout in the future.
*
* @param context
* @param keyId
* @param passphrase
*/
public static void addCachedPassphrase(Context context, long keyId, String passphrase) {
Log.d(TAG, "cacheNewPassphrase() for " + keyId);
Intent intent = new Intent(context, PassphraseCacheService.class);
intent.setAction(ACTION_PASSPHRASE_CACHE_ADD);
intent.putExtra(EXTRA_TTL, Preferences.getPreferences(context).getPassPhraseCacheTtl());
intent.putExtra(EXTRA_PASSPHRASE, passphrase);
intent.putExtra(EXTRA_KEY_ID, keyId);
context.startService(intent);
}
/**
* Gets a cached passphrase from memory by sending an intent to the service. This method is
* designed to wait until the service returns the passphrase.
*
* @param context
* @param keyId
* @return passphrase or null (if no passphrase is cached for this keyId)
*/
public static String getCachedPassphrase(Context context, long keyId) {
Log.d(TAG, "getCachedPassphrase() get masterKeyId for " + keyId);
Intent intent = new Intent(context, PassphraseCacheService.class);
intent.setAction(ACTION_PASSPHRASE_CACHE_GET);
final Object mutex = new Object();
final Bundle returnBundle = new Bundle();
HandlerThread handlerThread = new HandlerThread("getPassphraseThread");
handlerThread.start();
Handler returnHandler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message message) {
if (message.obj != null) {
String passphrase = ((Bundle) message.obj).getString(EXTRA_PASSPHRASE);
returnBundle.putString(EXTRA_PASSPHRASE, passphrase);
}
synchronized (mutex) {
mutex.notify();
}
// quit handlerThread
getLooper().quit();
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
intent.putExtra(EXTRA_KEY_ID, keyId);
intent.putExtra(EXTRA_MESSENGER, messenger);
// send intent to this service
context.startService(intent);
// Wait on mutex until passphrase is returned to handlerThread
synchronized (mutex) {
try {
mutex.wait(3000);
} catch (InterruptedException e) {
}
}
if (returnBundle.containsKey(EXTRA_PASSPHRASE)) {
return returnBundle.getString(EXTRA_PASSPHRASE);
} else {
return null;
}
}
/**
* Internal implementation to get cached passphrase.
*
* @param keyId
* @return
*/
private String getCachedPassphraseImpl(long keyId) {
Log.d(TAG, "getCachedPassphraseImpl() get masterKeyId for " + keyId);
// try to get master key id which is used as an identifier for cached passphrases
long masterKeyId = keyId;
if (masterKeyId != Id.key.symmetric) {
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(this, keyId);
if (keyRing == null) {
return null;
}
PGPSecretKey masterKey = PgpKeyHelper.getMasterKey(keyRing);
if (masterKey == null) {
return null;
}
masterKeyId = masterKey.getKeyID();
}
Log.d(TAG, "getCachedPassphraseImpl() for masterKeyId " + masterKeyId);
// get cached passphrase
String cachedPassphrase = mPassphraseCache.get(masterKeyId);
if (cachedPassphrase == null) {
// if key has no passphrase -> cache and return empty passphrase
if (!hasPassphrase(this, masterKeyId)) {
Log.d(Constants.TAG, "Key has no passphrase! Caches and returns empty passphrase!");
addCachedPassphrase(this, masterKeyId, "");
return "";
} else {
return null;
}
}
// set it again to reset the cache life cycle
Log.d(TAG, "Cache passphrase again when getting it!");
addCachedPassphrase(this, masterKeyId, cachedPassphrase);
return cachedPassphrase;
}
/**
* Checks if key has a passphrase.
*
* @param secretKeyId
* @return true if it has a passphrase
*/
public static boolean hasPassphrase(Context context, long secretKeyId) {
// check if the key has no passphrase
try {
PGPSecretKey secretKey = PgpKeyHelper.getMasterKey(ProviderHelper
.getPGPSecretKeyRingByKeyId(context, secretKeyId));
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
"SC").build("".toCharArray());
PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor);
if (testKey != null) {
return false;
}
} catch (PGPException e) {
// silently catch
}
return true;
}
/**
* Register BroadcastReceiver that is unregistered when service is destroyed. This
* BroadcastReceiver hears on intents with ACTION_PASSPHRASE_CACHE_SERVICE to then timeout
* specific passphrases in memory.
*/
private void registerReceiver() {
if (mIntentReceiver == null) {
mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "Received broadcast...");
if (action.equals(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE)) {
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
timeout(context, keyId);
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
registerReceiver(mIntentReceiver, filter);
}
}
/**
* Build pending intent that is executed by alarm manager to time out a specific passphrase
*
* @param context
* @param keyId
* @return
*/
private static PendingIntent buildIntent(Context context, long keyId) {
Intent intent = new Intent(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
intent.putExtra(EXTRA_KEY_ID, keyId);
PendingIntent sender = PendingIntent.getBroadcast(context, REQUEST_ID, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
return sender;
}
/**
* Executed when service is started by intent
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand()");
// register broadcastreceiver
registerReceiver();
if (intent != null && intent.getAction() != null) {
if (ACTION_PASSPHRASE_CACHE_ADD.equals(intent.getAction())) {
long ttl = intent.getLongExtra(EXTRA_TTL, DEFAULT_TTL);
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
String passphrase = intent.getStringExtra(EXTRA_PASSPHRASE);
Log.d(TAG,
"Received ACTION_PASSPHRASE_CACHE_ADD intent in onStartCommand() with keyId: "
+ keyId + ", ttl: " + ttl);
// add keyId and passphrase to memory
mPassphraseCache.put(keyId, passphrase);
if (ttl > 0) {
// register new alarm with keyId for this passphrase
long triggerTime = new Date().getTime() + (ttl * 1000);
AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
am.set(AlarmManager.RTC_WAKEUP, triggerTime, buildIntent(this, keyId));
}
} else if (ACTION_PASSPHRASE_CACHE_GET.equals(intent.getAction())) {
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
Messenger messenger = intent.getParcelableExtra(EXTRA_MESSENGER);
String passphrase = getCachedPassphraseImpl(keyId);
Message msg = Message.obtain();
Bundle bundle = new Bundle();
bundle.putString(EXTRA_PASSPHRASE, passphrase);
msg.obj = bundle;
try {
messenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "Sending message failed", e);
}
} else {
Log.e(Constants.TAG, "Intent or Intent Action not supported!");
}
}
return START_STICKY;
}
/**
* Called when one specific passphrase for keyId timed out
*
* @param context
* @param keyId
*/
private void timeout(Context context, long keyId) {
// remove passphrase corresponding to keyId from memory
mPassphraseCache.remove(keyId);
Log.d(TAG, "Timeout of keyId " + keyId + ", removed from memory!");
// stop whole service if no cached passphrases remaining
if (mPassphraseCache.isEmpty()) {
Log.d(TAG, "No passphrases remaining in memory, stopping service!");
stopSelf();
}
}
@Override
public void onCreate() {
super.onCreate();
mContext = this;
Log.d(Constants.TAG, "PassphraseCacheService, onCreate()");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(Constants.TAG, "PassphraseCacheService, onDestroy()");
unregisterReceiver(mIntentReceiver);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class PassphraseCacheBinder extends Binder {
public PassphraseCacheService getService() {
return PassphraseCacheService.this;
}
}
private final IBinder mBinder = new PassphraseCacheBinder();
}

View File

@@ -0,0 +1,10 @@
package org.sufficientlysecure.keychain.service.exception;
public class NoUserIdsException extends Exception {
private static final long serialVersionUID = 7009311527126696207L;
public NoUserIdsException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package org.sufficientlysecure.keychain.service.exception;
public class UserInteractionRequiredException extends Exception {
private static final long serialVersionUID = -60128148603511936L;
public UserInteractionRequiredException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package org.sufficientlysecure.keychain.service.exception;
public class WrongPackageSignatureException extends Exception {
private static final long serialVersionUID = -8294642703122196028L;
public WrongPackageSignatureException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package org.sufficientlysecure.keychain.service.exception;
public class WrongPassphraseException extends Exception {
private static final long serialVersionUID = -5309689232853485740L;
public WrongPassphraseException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.openpgp.PGPEncryptedData;
import org.sufficientlysecure.keychain.Id;
public class AppSettings {
private String packageName;
private byte[] packageSignature;
private long keyId = Id.key.none;
private int encryptionAlgorithm;
private int hashAlgorithm;
private int compression;
public AppSettings() {
}
public AppSettings(String packageName, byte[] packageSignature) {
super();
this.packageName = packageName;
this.packageSignature = packageSignature;
// defaults:
this.encryptionAlgorithm = PGPEncryptedData.AES_256;
this.hashAlgorithm = HashAlgorithmTags.SHA512;
this.compression = Id.choice.compression.zlib;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public byte[] getPackageSignature() {
return packageSignature;
}
public void setPackageSignature(byte[] packageSignature) {
this.packageSignature = packageSignature;
}
public long getKeyId() {
return keyId;
}
public void setKeyId(long scretKeyId) {
this.keyId = scretKeyId;
}
public int getEncryptionAlgorithm() {
return encryptionAlgorithm;
}
public void setEncryptionAlgorithm(int encryptionAlgorithm) {
this.encryptionAlgorithm = encryptionAlgorithm;
}
public int getHashAlgorithm() {
return hashAlgorithm;
}
public void setHashAlgorithm(int hashAlgorithm) {
this.hashAlgorithm = hashAlgorithm;
}
public int getCompression() {
return compression;
}
public void setCompression(int compression) {
this.compression = compression;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
public class AppSettingsActivity extends SherlockFragmentActivity {
private Uri mAppUri;
private AppSettingsFragment mSettingsFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Inflate a "Done" custom action bar
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.api_settings_save,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// "Done"
save();
}
});
setContentView(R.layout.api_app_settings_activity);
mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
R.id.api_app_settings_fragment);
Intent intent = getIntent();
mAppUri = intent.getData();
if (mAppUri == null) {
Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!");
finish();
return;
} else {
Log.d(Constants.TAG, "uri: " + mAppUri);
loadData(mAppUri);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getSupportMenuInflater().inflate(R.menu.api_app_settings, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_api_settings_revoke:
revokeAccess();
return true;
case R.id.menu_api_settings_cancel:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void loadData(Uri appUri) {
AppSettings settings = ProviderHelper.getApiAppSettings(this, appUri);
mSettingsFragment.setAppSettings(settings);
}
private void revokeAccess() {
if (getContentResolver().delete(mAppUri, null, null) <= 0) {
throw new RuntimeException();
}
finish();
}
private void save() {
ProviderHelper.updateApiApp(this, mSettingsFragment.getAppSettings(), mAppUri);
finish();
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.spongycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.SelectSecretKeyLayoutFragment;
import org.sufficientlysecure.keychain.ui.adapter.KeyValueSpinnerAdapter;
import org.sufficientlysecure.keychain.util.AlgorithmNames;
import org.sufficientlysecure.keychain.util.Log;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class AppSettingsFragment extends Fragment implements
SelectSecretKeyLayoutFragment.SelectSecretKeyCallback {
// model
private AppSettings appSettings;
// view
private LinearLayout mAdvancedSettingsContainer;
private BootstrapButton mAdvancedSettingsButton;
private TextView mAppNameView;
private ImageView mAppIconView;
private Spinner mEncryptionAlgorithm;
private Spinner mHashAlgorithm;
private Spinner mCompression;
private TextView mPackageName;
private TextView mPackageSignature;
private SelectSecretKeyLayoutFragment mSelectKeyFragment;
KeyValueSpinnerAdapter encryptionAdapter;
KeyValueSpinnerAdapter hashAdapter;
KeyValueSpinnerAdapter compressionAdapter;
public AppSettings getAppSettings() {
return appSettings;
}
public void setAppSettings(AppSettings appSettings) {
this.appSettings = appSettings;
setPackage(appSettings.getPackageName());
mPackageName.setText(appSettings.getPackageName());
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(appSettings.getPackageSignature());
byte[] digest = md.digest();
String signature = new String(Hex.encode(digest));
mPackageSignature.setText(signature);
} catch (NoSuchAlgorithmException e) {
Log.e(Constants.TAG, "Should not happen!", e);
}
mSelectKeyFragment.selectKey(appSettings.getKeyId());
mEncryptionAlgorithm.setSelection(encryptionAdapter.getPosition(appSettings
.getEncryptionAlgorithm()));
mHashAlgorithm.setSelection(hashAdapter.getPosition(appSettings.getHashAlgorithm()));
mCompression.setSelection(compressionAdapter.getPosition(appSettings.getCompression()));
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.api_app_settings_fragment, container, false);
initView(view);
return view;
}
private void initView(View view) {
mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getFragmentManager().findFragmentById(
R.id.api_app_settings_select_key_fragment);
mSelectKeyFragment.setCallback(this);
mAdvancedSettingsButton = (BootstrapButton) view
.findViewById(R.id.api_app_settings_advanced_button);
mAdvancedSettingsContainer = (LinearLayout) view
.findViewById(R.id.api_app_settings_advanced);
mAppNameView = (TextView) view.findViewById(R.id.api_app_settings_app_name);
mAppIconView = (ImageView) view.findViewById(R.id.api_app_settings_app_icon);
mEncryptionAlgorithm = (Spinner) view
.findViewById(R.id.api_app_settings_encryption_algorithm);
mHashAlgorithm = (Spinner) view.findViewById(R.id.api_app_settings_hash_algorithm);
mCompression = (Spinner) view.findViewById(R.id.api_app_settings_compression);
mPackageName = (TextView) view.findViewById(R.id.api_app_settings_package_name);
mPackageSignature = (TextView) view.findViewById(R.id.api_app_settings_package_signature);
AlgorithmNames algorithmNames = new AlgorithmNames(getActivity());
encryptionAdapter = new KeyValueSpinnerAdapter(getActivity(),
algorithmNames.getEncryptionNames());
mEncryptionAlgorithm.setAdapter(encryptionAdapter);
mEncryptionAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
appSettings.setEncryptionAlgorithm((int) id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
hashAdapter = new KeyValueSpinnerAdapter(getActivity(), algorithmNames.getHashNames());
mHashAlgorithm.setAdapter(hashAdapter);
mHashAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
appSettings.setHashAlgorithm((int) id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
compressionAdapter = new KeyValueSpinnerAdapter(getActivity(),
algorithmNames.getCompressionNames());
mCompression.setAdapter(compressionAdapter);
mCompression.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
appSettings.setCompression((int) id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
final Animation visibleAnimation = new AlphaAnimation(0.0f, 1.0f);
visibleAnimation.setDuration(250);
final Animation invisibleAnimation = new AlphaAnimation(1.0f, 0.0f);
invisibleAnimation.setDuration(250);
// TODO: Better: collapse/expand animation
// final Animation animation2 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f,
// Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, -1.0f,
// Animation.RELATIVE_TO_SELF, 0.0f);
// animation2.setDuration(150);
mAdvancedSettingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mAdvancedSettingsContainer.getVisibility() == View.VISIBLE) {
mAdvancedSettingsContainer.startAnimation(invisibleAnimation);
mAdvancedSettingsContainer.setVisibility(View.GONE);
mAdvancedSettingsButton.setText(getString(R.string.api_settings_show_advanced));
mAdvancedSettingsButton.setLeftIcon("fa-caret-up");
} else {
mAdvancedSettingsContainer.startAnimation(visibleAnimation);
mAdvancedSettingsContainer.setVisibility(View.VISIBLE);
mAdvancedSettingsButton.setText(getString(R.string.api_settings_hide_advanced));
mAdvancedSettingsButton.setLeftIcon("fa-caret-down");
}
}
});
}
private void setPackage(String packageName) {
PackageManager pm = getActivity().getApplicationContext().getPackageManager();
// get application name and icon from package manager
String appName = null;
Drawable appIcon = null;
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
appName = (String) pm.getApplicationLabel(ai);
appIcon = pm.getApplicationIcon(ai);
} catch (final NameNotFoundException e) {
// fallback
appName = packageName;
}
mAppNameView.setText(appName);
mAppIconView.setImageDrawable(appIcon);
}
/**
* callback from select secret key fragment
*/
@Override
public void onKeySelected(long secretKeyId) {
appSettings.setKeyId(secretKeyId);
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.security.cert.X509Certificate;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openssl.PEMWriter;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.pgp.PgpToX509;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
public class ExtendedApiService extends RemoteService {
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private void selfSignedX509CertSafe(String subjAltNameURI, IExtendedApiCallback callback,
AppSettings appSettings) {
// TODO: for pgp keyrings with password
CallbackHandler pgpPwdCallbackHandler = new PgpToX509.PredefinedPasswordCallbackHandler("");
try {
long keyId = appSettings.getKeyId();
PGPSecretKey pgpSecretKey = PgpKeyHelper.getSigningKey(this, keyId);
PasswordCallback pgpSecKeyPasswordCallBack = new PasswordCallback("pgp passphrase?",
false);
pgpPwdCallbackHandler.handle(new Callback[] { pgpSecKeyPasswordCallBack });
PGPPrivateKey pgpPrivKey = pgpSecretKey.extractPrivateKey(
pgpSecKeyPasswordCallBack.getPassword(), Constants.BOUNCY_CASTLE_PROVIDER_NAME);
pgpSecKeyPasswordCallBack.clearPassword();
X509Certificate selfSignedCert = PgpToX509.createSelfSignedCert(pgpSecretKey,
pgpPrivKey, subjAltNameURI);
// Write x509cert and privKey into files
// FileOutputStream fosCert = context.openFileOutput(CERT_FILENAME,
// Context.MODE_PRIVATE);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
PEMWriter pemWriterCert = new PEMWriter(new PrintWriter(outStream));
pemWriterCert.writeObject(selfSignedCert);
pemWriterCert.close();
byte[] outputBytes = outStream.toByteArray();
callback.onSuccess(outputBytes);
} catch (Exception e) {
Log.e(Constants.TAG, "ExtendedApiService", e);
try {
callback.onError(e.getMessage());
} catch (RemoteException e1) {
Log.e(Constants.TAG, "ExtendedApiService", e);
}
}
// TODO: no private key at the moment! Don't give it to others
// PrivateKey privKey = pgpPrivKey.getKey();
// FileOutputStream fosKey = context.openFileOutput(PRIV_KEY_FILENAME,
// Context.MODE_PRIVATE);
// PEMWriter pemWriterKey = new PEMWriter(new PrintWriter(fosKey));
// pemWriterKey.writeObject(privKey);
// pemWriterKey.close();
}
private final IExtendedApiService.Stub mBinder = new IExtendedApiService.Stub() {
@Override
public void encrypt(byte[] inputBytes, String passphrase, IExtendedApiCallback callback)
throws RemoteException {
// TODO : implement
}
@Override
public void selfSignedX509Cert(final String subjAltNameURI,
final IExtendedApiCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
selfSignedX509CertSafe(subjAltNameURI, callback, settings);
}
};
checkAndEnqueue(r);
}
};
}

View File

@@ -0,0 +1,602 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.regex.Matcher;
import org.openintents.openpgp.IOpenPgpCallback;
import org.openintents.openpgp.IOpenPgpKeyIdsCallback;
import org.openintents.openpgp.IOpenPgpService;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.spongycastle.util.Arrays;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpOperation;
import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.exception.NoUserIdsException;
import org.sufficientlysecure.keychain.service.exception.UserInteractionRequiredException;
import org.sufficientlysecure.keychain.service.exception.WrongPassphraseException;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
public class OpenPgpService extends RemoteService {
private String getCachedPassphrase(long keyId, boolean allowUserInteraction)
throws UserInteractionRequiredException {
String passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), keyId);
if (passphrase == null) {
if (!allowUserInteraction) {
throw new UserInteractionRequiredException(
"Passphrase not found in cache, please enter your passphrase!");
}
Log.d(Constants.TAG, "No passphrase! Activity required!");
// start passphrase dialog
PassphraseActivityCallback callback = new PassphraseActivityCallback();
Bundle extras = new Bundle();
extras.putLong(RemoteServiceActivity.EXTRA_SECRET_KEY_ID, keyId);
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_CACHE_PASSPHRASE, callback,
extras);
if (callback.isSuccess()) {
Log.d(Constants.TAG, "New passphrase entered!");
// get again after it was entered
passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), keyId);
} else {
Log.d(Constants.TAG, "Passphrase dialog canceled!");
return null;
}
}
return passphrase;
}
public class PassphraseActivityCallback extends UserInputCallback {
private boolean success = false;
public boolean isSuccess() {
return success;
}
@Override
public void handleUserInput(Message msg) {
if (msg.arg1 == OKAY) {
success = true;
} else {
success = false;
}
}
};
/**
* Search database for key ids based on emails.
*
* @param encryptionUserIds
* @return
*/
private long[] getKeyIdsFromEmails(String[] encryptionUserIds, boolean allowUserInteraction)
throws UserInteractionRequiredException {
// find key ids to given emails in database
ArrayList<Long> keyIds = new ArrayList<Long>();
boolean missingUserIdsCheck = false;
boolean dublicateUserIdsCheck = false;
ArrayList<String> missingUserIds = new ArrayList<String>();
ArrayList<String> dublicateUserIds = new ArrayList<String>();
for (String email : encryptionUserIds) {
Uri uri = KeychainContract.KeyRings.buildPublicKeyRingsByEmailsUri(email);
Cursor cur = getContentResolver().query(uri, null, null, null, null);
if (cur.moveToFirst()) {
long id = cur.getLong(cur.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID));
keyIds.add(id);
} else {
missingUserIdsCheck = true;
missingUserIds.add(email);
Log.d(Constants.TAG, "user id missing");
}
if (cur.moveToNext()) {
dublicateUserIdsCheck = true;
dublicateUserIds.add(email);
Log.d(Constants.TAG, "more than one user id with the same email");
}
}
// convert to long[]
long[] keyIdsArray = new long[keyIds.size()];
for (int i = 0; i < keyIdsArray.length; i++) {
keyIdsArray[i] = keyIds.get(i);
}
// allow the user to verify pub key selection
if (allowUserInteraction && (missingUserIdsCheck || dublicateUserIdsCheck)) {
SelectPubKeysActivityCallback callback = new SelectPubKeysActivityCallback();
Bundle extras = new Bundle();
extras.putLongArray(RemoteServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray);
extras.putStringArrayList(RemoteServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds);
extras.putStringArrayList(RemoteServiceActivity.EXTRA_DUBLICATE_USER_IDS,
dublicateUserIds);
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_SELECT_PUB_KEYS, callback,
extras);
if (callback.isSuccess()) {
Log.d(Constants.TAG, "New selection of pub keys!");
keyIdsArray = callback.getPubKeyIds();
} else {
Log.d(Constants.TAG, "Pub key selection canceled!");
return null;
}
}
// if no user interaction is allow throw exceptions on duplicate or missing pub keys
if (!allowUserInteraction) {
if (missingUserIdsCheck)
throw new UserInteractionRequiredException(
"Pub keys for these user ids are missing:" + missingUserIds.toString());
if (dublicateUserIdsCheck)
throw new UserInteractionRequiredException(
"More than one pub key with these user ids exist:"
+ dublicateUserIds.toString());
}
if (keyIdsArray.length == 0) {
return null;
}
return keyIdsArray;
}
public class SelectPubKeysActivityCallback extends UserInputCallback {
public static final String PUB_KEY_IDS = "pub_key_ids";
private boolean success = false;
private long[] pubKeyIds;
public boolean isSuccess() {
return success;
}
public long[] getPubKeyIds() {
return pubKeyIds;
}
@Override
public void handleUserInput(Message msg) {
if (msg.arg1 == OKAY) {
success = true;
pubKeyIds = msg.getData().getLongArray(PUB_KEY_IDS);
} else {
success = false;
}
}
};
private synchronized void getKeyIdsSafe(String[] userIds, boolean allowUserInteraction,
IOpenPgpKeyIdsCallback callback, AppSettings appSettings) {
try {
long[] keyIds = getKeyIdsFromEmails(userIds, allowUserInteraction);
if (keyIds == null) {
throw new NoUserIdsException("No user ids!");
}
callback.onSuccess(keyIds);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (NoUserIdsException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_USER_IDS, e.getMessage());
} catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
private synchronized void encryptAndSignSafe(OpenPgpData inputData,
final OpenPgpData outputData, long[] keyIds, boolean allowUserInteraction,
IOpenPgpCallback callback, AppSettings appSettings, boolean sign) {
try {
// TODO: other options of OpenPgpData!
byte[] inputBytes = getInput(inputData);
boolean asciiArmor = false;
if (outputData.getType() == OpenPgpData.TYPE_STRING) {
asciiArmor = true;
}
// add own key for encryption
keyIds = Arrays.copyOf(keyIds, keyIds.length + 1);
keyIds[keyIds.length - 1] = appSettings.getKeyId();
// build InputData and write into OutputStream
InputStream inputStream = new ByteArrayInputStream(inputBytes);
long inputLength = inputBytes.length;
InputData inputDt = new InputData(inputStream, inputLength);
OutputStream outputStream = new ByteArrayOutputStream();
PgpOperation operation = new PgpOperation(getContext(), null, inputDt, outputStream);
if (sign) {
String passphrase = getCachedPassphrase(appSettings.getKeyId(),
allowUserInteraction);
if (passphrase == null) {
throw new WrongPassphraseException("No or wrong passphrase!");
}
operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
appSettings.getEncryptionAlgorithm(), appSettings.getKeyId(),
appSettings.getHashAlgorithm(), true, passphrase);
} else {
operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
appSettings.getEncryptionAlgorithm(), Id.key.none,
appSettings.getHashAlgorithm(), true, null);
}
outputStream.close();
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
OpenPgpData output = null;
if (asciiArmor) {
output = new OpenPgpData(new String(outputBytes));
} else {
output = new OpenPgpData(outputBytes);
}
// return over handler on client side
callback.onSuccess(output, null);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (WrongPassphraseException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
} catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
// TODO: asciiArmor?!
private void signSafe(byte[] inputBytes, boolean allowUserInteraction,
IOpenPgpCallback callback, AppSettings appSettings) {
try {
// build InputData and write into OutputStream
InputStream inputStream = new ByteArrayInputStream(inputBytes);
long inputLength = inputBytes.length;
InputData inputData = new InputData(inputStream, inputLength);
OutputStream outputStream = new ByteArrayOutputStream();
String passphrase = getCachedPassphrase(appSettings.getKeyId(), allowUserInteraction);
if (passphrase == null) {
throw new WrongPassphraseException("No or wrong passphrase!");
}
PgpOperation operation = new PgpOperation(getContext(), null, inputData, outputStream);
operation.signText(appSettings.getKeyId(), passphrase, appSettings.getHashAlgorithm(),
Preferences.getPreferences(this).getForceV3Signatures());
outputStream.close();
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
OpenPgpData output = new OpenPgpData(new String(outputBytes));
// return over handler on client side
callback.onSuccess(output, null);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (WrongPassphraseException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
} catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
private synchronized void decryptAndVerifySafe(byte[] inputBytes, boolean allowUserInteraction,
IOpenPgpCallback callback, AppSettings appSettings) {
try {
// TODO: this is not really needed
// checked if it is text with BEGIN and END tags
String message = new String(inputBytes);
Log.d(Constants.TAG, "in: " + message);
boolean signedOnly = false;
Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(message);
if (matcher.matches()) {
Log.d(Constants.TAG, "PGP_MESSAGE matched");
message = matcher.group(1);
// replace non breakable spaces
message = message.replaceAll("\\xa0", " ");
// overwrite inputBytes
inputBytes = message.getBytes();
} else {
matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(message);
if (matcher.matches()) {
signedOnly = true;
Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
message = matcher.group(1);
// replace non breakable spaces
message = message.replaceAll("\\xa0", " ");
// overwrite inputBytes
inputBytes = message.getBytes();
} else {
Log.d(Constants.TAG, "Nothing matched! Binary?");
}
}
// END TODO
Log.d(Constants.TAG, "in: " + new String(inputBytes));
// TODO: This allows to decrypt messages with ALL secret keys, not only the one for the
// app, Fix this?
String passphrase = null;
if (!signedOnly) {
// BEGIN Get key
// TODO: this input stream is consumed after PgpMain.getDecryptionKeyId()... do it
// better!
InputStream inputStream2 = new ByteArrayInputStream(inputBytes);
// TODO: duplicates functions from DecryptActivity!
long secretKeyId;
try {
if (inputStream2.markSupported()) {
// should probably set this to the max size of two
// pgpF objects, if it even needs to be anything other
// than 0.
inputStream2.mark(200);
}
secretKeyId = PgpHelper.getDecryptionKeyId(this, inputStream2);
if (secretKeyId == Id.key.none) {
throw new PgpGeneralException(getString(R.string.error_no_secret_key_found));
}
} catch (NoAsymmetricEncryptionException e) {
if (inputStream2.markSupported()) {
inputStream2.reset();
}
secretKeyId = Id.key.symmetric;
if (!PgpOperation.hasSymmetricEncryption(this, inputStream2)) {
throw new PgpGeneralException(
getString(R.string.error_no_known_encryption_found));
}
// we do not support symmetric decryption from the API!
throw new Exception("Symmetric decryption is not supported!");
}
Log.d(Constants.TAG, "secretKeyId " + secretKeyId);
passphrase = getCachedPassphrase(secretKeyId, allowUserInteraction);
if (passphrase == null) {
throw new WrongPassphraseException("No or wrong passphrase!");
}
}
// build InputData and write into OutputStream
InputStream inputStream = new ByteArrayInputStream(inputBytes);
long inputLength = inputBytes.length;
InputData inputData = new InputData(inputStream, inputLength);
OutputStream outputStream = new ByteArrayOutputStream();
Bundle outputBundle;
PgpOperation operation = new PgpOperation(getContext(), null, inputData, outputStream);
if (signedOnly) {
// TODO: download missing keys from keyserver?
outputBundle = operation.verifyText(false);
} else {
outputBundle = operation.decryptAndVerify(passphrase, false);
}
outputStream.close();
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
// get signature informations from bundle
boolean signature = outputBundle.getBoolean(KeychainIntentService.RESULT_SIGNATURE);
OpenPgpSignatureResult sigResult = null;
if (signature) {
long signatureKeyId = outputBundle
.getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
String signatureUserId = outputBundle
.getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
boolean signatureSuccess = outputBundle
.getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS);
boolean signatureUnknown = outputBundle
.getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN);
int signatureStatus = OpenPgpSignatureResult.SIGNATURE_ERROR;
if (signatureSuccess) {
signatureStatus = OpenPgpSignatureResult.SIGNATURE_SUCCESS_TRUSTED;
} else if (signatureUnknown) {
signatureStatus = OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY;
}
sigResult = new OpenPgpSignatureResult(signatureStatus, signatureUserId,
signedOnly, signatureKeyId);
}
OpenPgpData output = new OpenPgpData(new String(outputBytes));
// return over handler on client side
callback.onSuccess(output, sigResult);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (WrongPassphraseException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
} catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
/**
* Returns error to IOpenPgpCallback
*
* @param callback
* @param errorId
* @param message
*/
private void callbackOpenPgpError(IOpenPgpCallback callback, int errorId, String message) {
try {
callback.onError(new OpenPgpError(0, message));
} catch (Exception t) {
Log.e(Constants.TAG,
"Exception while returning OpenPgpError to client via callback.onError()", t);
}
}
private void callbackOpenPgpError(IOpenPgpKeyIdsCallback callback, int errorId, String message) {
try {
callback.onError(new OpenPgpError(0, message));
} catch (Exception t) {
Log.e(Constants.TAG,
"Exception while returning OpenPgpError to client via callback.onError()", t);
}
}
private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() {
@Override
public void encrypt(final OpenPgpData input, final OpenPgpData output, final long[] keyIds,
final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
encryptAndSignSafe(input, output, keyIds, true, callback, settings, false);
}
};
checkAndEnqueue(r);
}
@Override
public void signAndEncrypt(final OpenPgpData input, final OpenPgpData output,
final long[] keyIds, final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
encryptAndSignSafe(input, output, keyIds, true, callback, settings, true);
}
};
checkAndEnqueue(r);
}
@Override
public void sign(final OpenPgpData input, final OpenPgpData output,
final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
signSafe(getInput(input), true, callback, settings);
}
};
checkAndEnqueue(r);
}
@Override
public void decryptAndVerify(final OpenPgpData input, final OpenPgpData output,
final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
decryptAndVerifySafe(getInput(input), true, callback, settings);
}
};
checkAndEnqueue(r);
}
@Override
public void getKeyIds(final String[] userIds, final boolean allowUserInteraction,
final IOpenPgpKeyIdsCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
getKeyIdsSafe(userIds, allowUserInteraction, callback, settings);
}
};
checkAndEnqueue(r);
}
};
private static byte[] getInput(OpenPgpData data) {
// TODO: support Uri and ParcelFileDescriptor
byte[] inBytes = null;
switch (data.getType()) {
case OpenPgpData.TYPE_STRING:
inBytes = data.getString().getBytes();
break;
case OpenPgpData.TYPE_BYTE_ARRAY:
inBytes = data.getBytes();
break;
default:
Log.e(Constants.TAG, "Uri and ParcelFileDescriptor not supported right now!");
break;
}
return inBytes;
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class RegisteredAppsAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private PackageManager pm;
public RegisteredAppsAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
pm = context.getApplicationContext().getPackageManager();
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
TextView text = (TextView) view.findViewById(R.id.api_apps_adapter_item_name);
ImageView icon = (ImageView) view.findViewById(R.id.api_apps_adapter_item_icon);
String packageName = cursor.getString(cursor.getColumnIndex(ApiApps.PACKAGE_NAME));
if (packageName != null) {
// get application name
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
text.setText(pm.getApplicationLabel(ai));
icon.setImageDrawable(pm.getApplicationIcon(ai));
} catch (final NameNotFoundException e) {
// fallback
text.setText(packageName);
}
} else {
// fallback
text.setText(packageName);
}
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(R.layout.api_apps_adapter_list_item, null);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.DrawerActivity;
import android.os.Bundle;
public class RegisteredAppsListActivity extends DrawerActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.api_apps_list_activity);
setupDrawerNavigation(savedInstanceState);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import com.actionbarsherlock.app.SherlockListFragment;
public class RegisteredAppsListFragment extends SherlockListFragment implements
LoaderManager.LoaderCallbacks<Cursor> {
// This is the Adapter being used to display the list's data.
RegisteredAppsAdapter mAdapter;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getListView().setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
// edit app settings
Intent intent = new Intent(getActivity(), AppSettingsActivity.class);
intent.setData(ContentUris.withAppendedId(KeychainContract.ApiApps.CONTENT_URI, id));
startActivity(intent);
}
});
// Give some text to display if there is no data. In a real
// application this would come from a resource.
setEmptyText(getString(R.string.api_no_apps));
// We have a menu item to show in action bar.
setHasOptionsMenu(true);
// Create an empty adapter we will use to display the loaded data.
mAdapter = new RegisteredAppsAdapter(getActivity(), null, 0);
setListAdapter(mAdapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
// These are the Contacts rows that we will retrieve.
static final String[] PROJECTION = new String[] { ApiApps._ID, ApiApps.PACKAGE_NAME };
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri = ApiApps.CONTENT_URI;
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null,
ApiApps.PACKAGE_NAME + " COLLATE LOCALIZED ASC");
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
}

View File

@@ -0,0 +1,354 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.exception.WrongPackageSignatureException;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.PausableThreadPoolExecutor;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
/**
* Abstract service class for remote APIs that handle app registration and user input.
*/
public abstract class RemoteService extends Service {
Context mContext;
private final ArrayBlockingQueue<Runnable> mPoolQueue = new ArrayBlockingQueue<Runnable>(100);
// TODO: Are these parameters okay?
private PausableThreadPoolExecutor mThreadPool = new PausableThreadPoolExecutor(2, 4, 10,
TimeUnit.SECONDS, mPoolQueue);
private final Object userInputLock = new Object();
/**
* Override handleUserInput() to handle OKAY (1) and CANCEL (0). After handling the waiting
* threads will be notified and the queue resumed
*/
protected class UserInputCallback extends BaseCallback {
public void handleUserInput(Message msg) {
}
@Override
public boolean handleMessage(Message msg) {
handleUserInput(msg);
// resume
synchronized (userInputLock) {
userInputLock.notifyAll();
}
mThreadPool.resume();
return true;
}
}
/**
* Extends Handler.Callback with OKAY (1), CANCEL (0) variables
*/
private class BaseCallback implements Handler.Callback {
public static final int OKAY = 1;
public static final int CANCEL = 0;
@Override
public boolean handleMessage(Message msg) {
return false;
}
}
public Context getContext() {
return mContext;
}
/**
* Should be used from Stub implementations of AIDL interfaces to enqueue a runnable for
* execution
*
* @param r
*/
protected void checkAndEnqueue(Runnable r) {
try {
if (isCallerAllowed(false)) {
mThreadPool.execute(r);
Log.d(Constants.TAG, "Enqueued runnable…");
} else {
String[] callingPackages = getPackageManager().getPackagesForUid(
Binder.getCallingUid());
// TODO: currently simply uses first entry
String packageName = callingPackages[0];
byte[] packageSignature;
try {
packageSignature = getPackageSignature(packageName);
} catch (NameNotFoundException e) {
Log.e(Constants.TAG, "Should not happen, returning!", e);
return;
}
Log.e(Constants.TAG,
"Not allowed to use service! Starting activity for registration!");
Bundle extras = new Bundle();
extras.putString(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName);
extras.putByteArray(RemoteServiceActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature);
RegisterActivityCallback callback = new RegisterActivityCallback();
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_REGISTER, callback,
extras);
if (callback.isAllowed()) {
mThreadPool.execute(r);
Log.d(Constants.TAG, "Enqueued runnable…");
} else {
Log.d(Constants.TAG, "User disallowed app!");
}
}
} catch (WrongPackageSignatureException e) {
Log.e(Constants.TAG, e.getMessage());
Bundle extras = new Bundle();
extras.putString(RemoteServiceActivity.EXTRA_ERROR_MESSAGE,
getString(R.string.api_error_wrong_signature));
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_ERROR_MESSAGE, null, extras);
}
}
private byte[] getPackageSignature(String packageName) throws NameNotFoundException {
PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
PackageManager.GET_SIGNATURES);
Signature[] signatures = pkgInfo.signatures;
// TODO: Only first signature?!
byte[] packageSignature = signatures[0].toByteArray();
return packageSignature;
}
/**
* Locks current thread and pauses execution of runnables and starts activity for user input
*
* @param action
* @param messenger
* @param extras
*/
protected void pauseAndStartUserInteraction(String action, BaseCallback callback, Bundle extras) {
synchronized (userInputLock) {
mThreadPool.pause();
Log.d(Constants.TAG, "starting activity...");
Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(action);
Messenger messenger = new Messenger(new Handler(getMainLooper(), callback));
extras.putParcelable(RemoteServiceActivity.EXTRA_MESSENGER, messenger);
intent.putExtras(extras);
startActivity(intent);
// lock current thread for user input
try {
userInputLock.wait();
} catch (InterruptedException e) {
Log.e(Constants.TAG, "CryptoService", e);
}
}
}
/**
* Retrieves AppSettings from database for the application calling this remote service
*
* @return
*/
protected AppSettings getAppSettings() {
String[] callingPackages = getPackageManager().getPackagesForUid(Binder.getCallingUid());
// get app settings for this package
for (int i = 0; i < callingPackages.length; i++) {
String currentPkg = callingPackages[i];
Uri uri = KeychainContract.ApiApps.buildByPackageNameUri(currentPkg);
AppSettings settings = ProviderHelper.getApiAppSettings(this, uri);
if (settings != null)
return settings;
}
return null;
}
class RegisterActivityCallback extends BaseCallback {
public static final String PACKAGE_NAME = "package_name";
private boolean allowed = false;
private String packageName;
public boolean isAllowed() {
return allowed;
}
public String getPackageName() {
return packageName;
}
@Override
public boolean handleMessage(Message msg) {
if (msg.arg1 == OKAY) {
allowed = true;
packageName = msg.getData().getString(PACKAGE_NAME);
// resume threads
try {
if (isPackageAllowed(packageName)) {
synchronized (userInputLock) {
userInputLock.notifyAll();
}
mThreadPool.resume();
} else {
// Should not happen!
Log.e(Constants.TAG, "Should not happen! Emergency shutdown!");
mThreadPool.shutdownNow();
}
} catch (WrongPackageSignatureException e) {
Log.e(Constants.TAG, e.getMessage());
Bundle extras = new Bundle();
extras.putString(RemoteServiceActivity.EXTRA_ERROR_MESSAGE,
getString(R.string.api_error_wrong_signature));
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_ERROR_MESSAGE, null,
extras);
}
} else {
allowed = false;
synchronized (userInputLock) {
userInputLock.notifyAll();
}
mThreadPool.resume();
}
return true;
}
}
/**
* Checks if process that binds to this service (i.e. the package name corresponding to the
* process) is in the list of allowed package names.
*
* @param allowOnlySelf
* allow only Keychain app itself
* @return true if process is allowed to use this service
* @throws WrongPackageSignatureException
*/
private boolean isCallerAllowed(boolean allowOnlySelf) throws WrongPackageSignatureException {
return isUidAllowed(Binder.getCallingUid(), allowOnlySelf);
}
private boolean isUidAllowed(int uid, boolean allowOnlySelf)
throws WrongPackageSignatureException {
if (android.os.Process.myUid() == uid) {
return true;
}
if (allowOnlySelf) { // barrier
return false;
}
String[] callingPackages = getPackageManager().getPackagesForUid(uid);
// is calling package allowed to use this service?
for (int i = 0; i < callingPackages.length; i++) {
String currentPkg = callingPackages[i];
if (isPackageAllowed(currentPkg)) {
return true;
}
}
Log.d(Constants.TAG, "Caller is NOT allowed!");
return false;
}
/**
* Checks if packageName is a registered app for the API. Does not return true for own package!
*
* @param packageName
* @return
* @throws WrongPackageSignatureException
*/
private boolean isPackageAllowed(String packageName) throws WrongPackageSignatureException {
Log.d(Constants.TAG, "packageName: " + packageName);
ArrayList<String> allowedPkgs = ProviderHelper.getRegisteredApiApps(this);
Log.d(Constants.TAG, "allowed: " + allowedPkgs);
// check if package is allowed to use our service
if (allowedPkgs.contains(packageName)) {
Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
// check package signature
byte[] currentSig;
try {
currentSig = getPackageSignature(packageName);
} catch (NameNotFoundException e) {
throw new WrongPackageSignatureException(e.getMessage());
}
byte[] storedSig = ProviderHelper.getApiAppSignature(this, packageName);
if (Arrays.equals(currentSig, storedSig)) {
Log.d(Constants.TAG,
"Package signature is correct! (equals signature from database)");
return true;
} else {
throw new WrongPackageSignatureException(
"PACKAGE NOT ALLOWED! Signature wrong! (Signature not equals signature from database)");
}
}
return false;
}
@Override
public void onCreate() {
super.onCreate();
mContext = this;
}
}

View File

@@ -0,0 +1,352 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.util.ArrayList;
import org.sufficientlysecure.htmltextview.HtmlTextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.SelectPublicKeyFragment;
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.view.View;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
public class RemoteServiceActivity extends SherlockFragmentActivity {
public static final String ACTION_REGISTER = Constants.INTENT_PREFIX + "API_ACTIVITY_REGISTER";
public static final String ACTION_CACHE_PASSPHRASE = Constants.INTENT_PREFIX
+ "API_ACTIVITY_CACHE_PASSPHRASE";
public static final String ACTION_SELECT_PUB_KEYS = Constants.INTENT_PREFIX
+ "API_ACTIVITY_SELECT_PUB_KEYS";
public static final String ACTION_ERROR_MESSAGE = Constants.INTENT_PREFIX
+ "API_ACTIVITY_ERROR_MESSAGE";
public static final String EXTRA_MESSENGER = "messenger";
// passphrase action
public static final String EXTRA_SECRET_KEY_ID = "secret_key_id";
// register action
public static final String EXTRA_PACKAGE_NAME = "package_name";
public static final String EXTRA_PACKAGE_SIGNATURE = "package_signature";
// select pub keys action
public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids";
public static final String EXTRA_MISSING_USER_IDS = "missing_user_ids";
public static final String EXTRA_DUBLICATE_USER_IDS = "dublicate_user_ids";
// error message
public static final String EXTRA_ERROR_MESSAGE = "error_message";
private Messenger mMessenger;
// register view
private AppSettingsFragment mSettingsFragment;
// select pub keys view
private SelectPublicKeyFragment mSelectFragment;
// has the user clicked one of the buttons
// or do we need to handle the callback in onStop()
private boolean finishHandled;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleActions(getIntent(), savedInstanceState);
}
@Override
protected void onStop() {
super.onStop();
if (!finishHandled) {
Message msg = Message.obtain();
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
}
}
protected void handleActions(Intent intent, Bundle savedInstanceState) {
finishHandled = false;
String action = intent.getAction();
Bundle extras = intent.getExtras();
if (extras == null) {
extras = new Bundle();
}
mMessenger = extras.getParcelable(EXTRA_MESSENGER);
/**
* com.android.crypto actions
*/
if (ACTION_REGISTER.equals(action)) {
final String packageName = extras.getString(EXTRA_PACKAGE_NAME);
final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE);
// Inflate a "Done"/"Cancel" custom action bar view
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.api_register_allow,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// Allow
// user needs to select a key!
if (mSettingsFragment.getAppSettings().getKeyId() == Id.key.none) {
Toast.makeText(RemoteServiceActivity.this,
R.string.api_register_error_select_key, Toast.LENGTH_LONG)
.show();
} else {
ProviderHelper.insertApiApp(RemoteServiceActivity.this,
mSettingsFragment.getAppSettings());
Message msg = Message.obtain();
msg.arg1 = RemoteService.RegisterActivityCallback.OKAY;
Bundle data = new Bundle();
data.putString(RemoteService.RegisterActivityCallback.PACKAGE_NAME,
packageName);
msg.setData(data);
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
}
}
}, R.string.api_register_disallow, new View.OnClickListener() {
@Override
public void onClick(View v) {
// Disallow
Message msg = Message.obtain();
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
}
});
setContentView(R.layout.api_app_register_activity);
mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
R.id.api_app_settings_fragment);
AppSettings settings = new AppSettings(packageName, packageSignature);
mSettingsFragment.setAppSettings(settings);
} else if (ACTION_CACHE_PASSPHRASE.equals(action)) {
long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID);
showPassphraseDialog(secretKeyId);
} else if (ACTION_SELECT_PUB_KEYS.equals(action)) {
long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
ArrayList<String> missingUserIds = intent
.getStringArrayListExtra(EXTRA_MISSING_USER_IDS);
ArrayList<String> dublicateUserIds = intent
.getStringArrayListExtra(EXTRA_DUBLICATE_USER_IDS);
String text = new String();
text += "<b>" + getString(R.string.api_select_pub_keys_text) + "</b>";
text += "<br/><br/>";
if (missingUserIds != null && missingUserIds.size() > 0) {
text += getString(R.string.api_select_pub_keys_missing_text);
text += "<br/>";
text += "<ul>";
for (String userId : missingUserIds) {
text += "<li>" + userId + "</li>";
}
text += "</ul>";
text += "<br/>";
}
if (dublicateUserIds != null && dublicateUserIds.size() > 0) {
text += getString(R.string.api_select_pub_keys_dublicates_text);
text += "<br/>";
text += "<ul>";
for (String userId : dublicateUserIds) {
text += "<li>" + userId + "</li>";
}
text += "</ul>";
}
// Inflate a "Done"/"Cancel" custom action bar view
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// ok
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.OKAY;
Bundle data = new Bundle();
data.putLongArray(
OpenPgpService.SelectPubKeysActivityCallback.PUB_KEY_IDS,
mSelectFragment.getSelectedMasterKeyIds());
msg.setData(data);
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
}
}, R.string.btn_do_not_save, new View.OnClickListener() {
@Override
public void onClick(View v) {
// cancel
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.CANCEL;
;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
}
});
setContentView(R.layout.api_app_select_pub_keys_activity);
// set text on view
HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_select_pub_keys_text);
textView.setHtmlFromString(text);
/* Load select pub keys fragment */
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.api_select_pub_keys_fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of the fragment
mSelectFragment = SelectPublicKeyFragment.newInstance(selectedMasterKeyIds);
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.api_select_pub_keys_fragment_container, mSelectFragment).commit();
}
} else if (ACTION_ERROR_MESSAGE.equals(action)) {
String errorMessage = intent.getStringExtra(EXTRA_ERROR_MESSAGE);
String text = new String();
text += "<font color=\"red\">" + errorMessage + "</font>";
// Inflate a "Done" custom action bar view
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.btn_okay,
new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
setContentView(R.layout.api_app_error_message);
// set text on view
HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_app_error_message_text);
textView.setHtmlFromString(text);
} else {
Log.e(Constants.TAG, "Wrong action!");
finish();
}
}
/**
* Shows passphrase dialog to cache a new passphrase the user enters for using it later for
* encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
* for a symmetric passphrase
*/
private void showPassphraseDialog(long secretKeyId) {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.PassphraseActivityCallback.OKAY;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
} else {
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.PassphraseActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
}
finishHandled = true;
finish();
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
try {
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
messenger, secretKeyId);
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
} catch (PgpGeneralException e) {
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
// send message to handler to start encryption directly
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
}
}
}

View File

@@ -0,0 +1,846 @@
/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.regex.Matcher;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import org.sufficientlysecure.keychain.helper.FileHelper;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.pgp.PgpOperation;
import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.LookupUnknownKeyDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.AnimationUtils;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewFlipper;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.beardedhen.androidbootstrap.BootstrapButton;
@SuppressLint("NewApi")
public class DecryptActivity extends DrawerActivity {
/* Intents */
// without permission
public static final String ACTION_DECRYPT = Constants.INTENT_PREFIX + "DECRYPT";
/* EXTRA keys for input */
public static final String EXTRA_TEXT = "text";
private long mSignatureKeyId = 0;
private boolean mReturnResult = false;
private boolean mSignedOnly = false;
private boolean mAssumeSymmetricEncryption = false;
private EditText mMessage = null;
private LinearLayout mSignatureLayout = null;
private ImageView mSignatureStatusImage = null;
private TextView mUserId = null;
private TextView mUserIdRest = null;
private ViewFlipper mSource = null;
private TextView mSourceLabel = null;
private ImageView mSourcePrevious = null;
private ImageView mSourceNext = null;
private boolean mDecryptEnabled = true;
private String mDecryptString = "";
private boolean mReplyEnabled = true;
private String mReplyString = "";
private int mDecryptTarget;
private EditText mFilename = null;
private CheckBox mDeleteAfter = null;
private BootstrapButton mBrowse = null;
private String mInputFilename = null;
private String mOutputFilename = null;
private Uri mContentUri = null;
private boolean mReturnBinary = false;
private long mUnknownSignatureKeyId = 0;
private long mSecretKeyId = Id.key.none;
private FileDialogFragment mFileDialog;
private boolean mLookupUnknownKey = true;
private boolean mDecryptImmediately = false;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (mDecryptEnabled) {
menu.add(1, Id.menu.option.decrypt, 0, mDecryptString).setShowAsAction(
MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
}
if (mReplyEnabled) {
menu.add(1, Id.menu.option.reply, 1, mReplyString).setShowAsAction(
MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case Id.menu.option.decrypt: {
decryptClicked();
return true;
}
case Id.menu.option.reply: {
replyClicked();
return true;
}
default: {
return super.onOptionsItemSelected(item);
}
}
}
private void initView() {
mSource = (ViewFlipper) findViewById(R.id.source);
mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
mSourceNext = (ImageView) findViewById(R.id.sourceNext);
mSourcePrevious.setClickable(true);
mSourcePrevious.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
R.anim.push_right_in));
mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
R.anim.push_right_out));
mSource.showPrevious();
updateSource();
}
});
mSourceNext.setClickable(true);
OnClickListener nextSourceClickListener = new OnClickListener() {
public void onClick(View v) {
mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
R.anim.push_left_in));
mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
R.anim.push_left_out));
mSource.showNext();
updateSource();
}
};
mSourceNext.setOnClickListener(nextSourceClickListener);
mSourceLabel.setClickable(true);
mSourceLabel.setOnClickListener(nextSourceClickListener);
mMessage = (EditText) findViewById(R.id.message);
mSignatureLayout = (LinearLayout) findViewById(R.id.signature);
mSignatureStatusImage = (ImageView) findViewById(R.id.ic_signature_status);
mUserId = (TextView) findViewById(R.id.mainUserId);
mUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
// measure the height of the source_file view and set the message view's min height to that,
// so it fills mSource fully... bit of a hack.
View tmp = findViewById(R.id.sourceFile);
tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int height = tmp.getMeasuredHeight();
mMessage.setMinimumHeight(height);
mFilename = (EditText) findViewById(R.id.filename);
mBrowse = (BootstrapButton) findViewById(R.id.btn_browse);
mBrowse.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
FileHelper.openFile(DecryptActivity.this, mFilename.getText().toString(), "*/*",
Id.request.filename);
}
});
mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterDecryption);
// default: message source
mSource.setInAnimation(null);
mSource.setOutAnimation(null);
while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
mSource.showNext();
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.decrypt_activity);
// set actionbar without home button if called from another app
ActionBarHelper.setBackButton(this);
initView();
setupDrawerNavigation(savedInstanceState);
// Handle intent actions
handleActions(getIntent());
if (mSource.getCurrentView().getId() == R.id.sourceMessage
&& mMessage.getText().length() == 0) {
CharSequence clipboardText = ClipboardReflection.getClipboardText(this);
String data = "";
if (clipboardText != null) {
Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(clipboardText);
if (!matcher.matches()) {
matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(clipboardText);
}
if (matcher.matches()) {
data = matcher.group(1);
mMessage.setText(data);
Toast.makeText(this, R.string.using_clipboard_content, Toast.LENGTH_SHORT)
.show();
}
}
}
mSignatureLayout.setVisibility(View.GONE);
mSignatureLayout.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (mSignatureKeyId == 0) {
return;
}
PGPPublicKeyRing key = ProviderHelper.getPGPPublicKeyRingByKeyId(
DecryptActivity.this, mSignatureKeyId);
if (key != null) {
Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, mSignatureKeyId);
startActivity(intent);
}
}
});
mReplyEnabled = false;
// build new actionbar
invalidateOptionsMenu();
if (mReturnResult) {
mSourcePrevious.setClickable(false);
mSourcePrevious.setEnabled(false);
mSourcePrevious.setVisibility(View.INVISIBLE);
mSourceNext.setClickable(false);
mSourceNext.setEnabled(false);
mSourceNext.setVisibility(View.INVISIBLE);
mSourceLabel.setClickable(false);
mSourceLabel.setEnabled(false);
}
updateSource();
if (mDecryptImmediately
|| (mSource.getCurrentView().getId() == R.id.sourceMessage && (mMessage.getText()
.length() > 0 || mContentUri != null))) {
decryptClicked();
}
}
/**
* Handles all actions with this intent
*
* @param intent
*/
private void handleActions(Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
String type = intent.getType();
Uri uri = intent.getData();
if (extras == null) {
extras = new Bundle();
}
/*
* Android's Action
*/
if (Intent.ACTION_SEND.equals(action) && type != null) {
// When sending to Keychain Encrypt via share menu
if ("text/plain".equals(type)) {
// Plain text
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
// handle like normal text decryption, override action and extras to later
// execute ACTION_DECRYPT in main actions
extras.putString(EXTRA_TEXT, sharedText);
action = ACTION_DECRYPT;
}
} else {
// Binary via content provider (could also be files)
// override uri to get stream from send
uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
action = ACTION_DECRYPT;
}
} else if (Intent.ACTION_VIEW.equals(action)) {
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
// override action
action = ACTION_DECRYPT;
}
String textData = extras.getString(EXTRA_TEXT);
/**
* Main Actions
*/
if (ACTION_DECRYPT.equals(action) && textData != null) {
Log.d(Constants.TAG, "textData null, matching text ...");
Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(textData);
if (matcher.matches()) {
Log.d(Constants.TAG, "PGP_MESSAGE matched");
textData = matcher.group(1);
// replace non breakable spaces
textData = textData.replaceAll("\\xa0", " ");
mMessage.setText(textData);
} else {
matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(textData);
if (matcher.matches()) {
Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
textData = matcher.group(1);
// replace non breakable spaces
textData = textData.replaceAll("\\xa0", " ");
mMessage.setText(textData);
mDecryptString = getString(R.string.btn_verify);
// build new action bar
invalidateOptionsMenu();
} else {
Log.d(Constants.TAG, "Nothing matched!");
}
}
} else if (ACTION_DECRYPT.equals(action) && uri != null) {
// get file path from uri
String path = FileHelper.getPath(this, uri);
if (path != null) {
mInputFilename = path;
mFilename.setText(mInputFilename);
guessOutputFilename();
mSource.setInAnimation(null);
mSource.setOutAnimation(null);
while (mSource.getCurrentView().getId() != R.id.sourceFile) {
mSource.showNext();
}
} else {
Log.e(Constants.TAG,
"Direct binary data without actual file in filesystem is not supported. Please use the Remote Service API!");
Toast.makeText(this, R.string.error_only_files_are_supported, Toast.LENGTH_LONG)
.show();
// end activity
finish();
}
} else {
Log.e(Constants.TAG,
"Include the extra 'text' or an Uri with setData() in your Intent!");
}
}
private void guessOutputFilename() {
mInputFilename = mFilename.getText().toString();
File file = new File(mInputFilename);
String filename = file.getName();
if (filename.endsWith(".asc") || filename.endsWith(".gpg") || filename.endsWith(".pgp")) {
filename = filename.substring(0, filename.length() - 4);
}
mOutputFilename = Constants.path.APP_DIR + "/" + filename;
}
private void updateSource() {
switch (mSource.getCurrentView().getId()) {
case R.id.sourceFile: {
mSourceLabel.setText(R.string.label_file);
mDecryptString = getString(R.string.btn_decrypt);
// build new action bar
invalidateOptionsMenu();
break;
}
case R.id.sourceMessage: {
mSourceLabel.setText(R.string.label_message);
mDecryptString = getString(R.string.btn_decrypt);
// build new action bar
invalidateOptionsMenu();
break;
}
default: {
break;
}
}
}
private void decryptClicked() {
if (mSource.getCurrentView().getId() == R.id.sourceFile) {
mDecryptTarget = Id.target.file;
} else {
mDecryptTarget = Id.target.message;
}
initiateDecryption();
}
private void initiateDecryption() {
if (mDecryptTarget == Id.target.file) {
String currentFilename = mFilename.getText().toString();
if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
guessOutputFilename();
}
if (mInputFilename.equals("")) {
Toast.makeText(this, R.string.no_file_selected, Toast.LENGTH_SHORT).show();
return;
}
if (mInputFilename.startsWith("file")) {
File file = new File(mInputFilename);
if (!file.exists() || !file.isFile()) {
Toast.makeText(
this,
getString(R.string.error_message,
getString(R.string.error_file_not_found)), Toast.LENGTH_SHORT)
.show();
return;
}
}
}
if (mDecryptTarget == Id.target.message) {
String messageData = mMessage.getText().toString();
Matcher matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(messageData);
if (matcher.matches()) {
mSignedOnly = true;
decryptStart();
return;
}
}
// else treat it as an decrypted message/file
mSignedOnly = false;
getDecryptionKeyFromInputStream();
// if we need a symmetric passphrase or a passphrase to use a secret key ask for it
if (mSecretKeyId == Id.key.symmetric
|| PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) {
showPassphraseDialog();
} else {
if (mDecryptTarget == Id.target.file) {
askForOutputFilename();
} else {
decryptStart();
}
}
}
/**
* Shows passphrase dialog to cache a new passphrase the user enters for using it later for
* encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
* for a symmetric passphrase
*/
private void showPassphraseDialog() {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
if (mDecryptTarget == Id.target.file) {
askForOutputFilename();
} else {
decryptStart();
}
}
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
try {
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
messenger, mSecretKeyId);
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
} catch (PgpGeneralException e) {
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
// send message to handler to start encryption directly
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
}
}
/**
* TODO: Rework function, remove global variables
*/
private void getDecryptionKeyFromInputStream() {
InputStream inStream = null;
if (mContentUri != null) {
try {
inStream = getContentResolver().openInputStream(mContentUri);
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "File not found!", e);
Toast.makeText(this, getString(R.string.error_file_not_found, e.getMessage()),
Toast.LENGTH_SHORT).show();
}
} else if (mDecryptTarget == Id.target.file) {
// check if storage is ready
if (!FileHelper.isStorageMounted(mInputFilename)) {
Toast.makeText(this, getString(R.string.error_external_storage_not_ready),
Toast.LENGTH_SHORT).show();
return;
}
try {
inStream = new BufferedInputStream(new FileInputStream(mInputFilename));
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "File not found!", e);
Toast.makeText(this, getString(R.string.error_file_not_found, e.getMessage()),
Toast.LENGTH_SHORT).show();
}
} else {
inStream = new ByteArrayInputStream(mMessage.getText().toString().getBytes());
}
// get decryption key for this inStream
try {
try {
if (inStream.markSupported()) {
inStream.mark(200); // should probably set this to the max size of two pgpF
// objects, if it even needs to be anything other than 0.
}
mSecretKeyId = PgpHelper.getDecryptionKeyId(this, inStream);
if (mSecretKeyId == Id.key.none) {
throw new PgpGeneralException(getString(R.string.error_no_secret_key_found));
}
mAssumeSymmetricEncryption = false;
} catch (NoAsymmetricEncryptionException e) {
if (inStream.markSupported()) {
inStream.reset();
}
mSecretKeyId = Id.key.symmetric;
if (!PgpOperation.hasSymmetricEncryption(this, inStream)) {
throw new PgpGeneralException(
getString(R.string.error_no_known_encryption_found));
}
mAssumeSymmetricEncryption = true;
}
} catch (Exception e) {
Toast.makeText(this, getString(R.string.error_message, e.getMessage()),
Toast.LENGTH_SHORT).show();
}
}
private void replyClicked() {
Intent intent = new Intent(this, EncryptActivity.class);
intent.setAction(EncryptActivity.ACTION_ENCRYPT);
String data = mMessage.getText().toString();
data = data.replaceAll("(?m)^", "> ");
data = "\n\n" + data;
intent.putExtra(EncryptActivity.EXTRA_TEXT, data);
intent.putExtra(EncryptActivity.EXTRA_SIGNATURE_KEY_ID, mSecretKeyId);
intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, new long[] { mSignatureKeyId });
startActivity(intent);
}
private void askForOutputFilename() {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == FileDialogFragment.MESSAGE_OKAY) {
Bundle data = message.getData();
mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
decryptStart();
}
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
mFileDialog = FileDialogFragment.newInstance(messenger,
getString(R.string.title_decrypt_to_file),
getString(R.string.specify_file_to_decrypt_to), mOutputFilename, null);
mFileDialog.show(getSupportFragmentManager(), "fileDialog");
}
private void lookupUnknownKey(long unknownKeyId) {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_OKAY) {
// the result is handled by onActivityResult() as LookupUnknownKeyDialogFragment
// starts a new Intent which then returns data
} else if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_CANCEL) {
// decrypt again, but don't lookup unknown keys!
mLookupUnknownKey = false;
decryptStart();
}
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
LookupUnknownKeyDialogFragment lookupKeyDialog = LookupUnknownKeyDialogFragment
.newInstance(messenger, unknownKeyId);
lookupKeyDialog.show(getSupportFragmentManager(), "unknownKeyDialog");
}
private void decryptStart() {
Log.d(Constants.TAG, "decryptStart");
// Send all information needed to service to decrypt in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
// fill values for this action
Bundle data = new Bundle();
intent.setAction(KeychainIntentService.ACTION_DECRYPT_VERIFY);
// choose action based on input: decrypt stream, file or bytes
if (mContentUri != null) {
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM);
data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mContentUri);
} else if (mDecryptTarget == Id.target.file) {
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
+ mOutputFilename);
data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
} else {
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
String message = mMessage.getText().toString();
data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, message.getBytes());
}
data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId);
data.putBoolean(KeychainIntentService.DECRYPT_SIGNED_ONLY, mSignedOnly);
data.putBoolean(KeychainIntentService.DECRYPT_LOOKUP_UNKNOWN_KEY, mLookupUnknownKey);
data.putBoolean(KeychainIntentService.DECRYPT_RETURN_BYTES, mReturnBinary);
data.putBoolean(KeychainIntentService.DECRYPT_ASSUME_SYMMETRIC, mAssumeSymmetricEncryption);
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after encrypting is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
R.string.progress_decrypting, ProgressDialog.STYLE_HORIZONTAL) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
// get returned data bundle
Bundle returnData = message.getData();
// if key is unknown show lookup dialog
if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_LOOKUP_KEY)
&& mLookupUnknownKey) {
mUnknownSignatureKeyId = returnData
.getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
lookupUnknownKey(mUnknownSignatureKeyId);
return;
}
mSignatureKeyId = 0;
mSignatureLayout.setVisibility(View.GONE);
mReplyEnabled = false;
// build new action bar
invalidateOptionsMenu();
Toast.makeText(DecryptActivity.this, R.string.decryption_successful,
Toast.LENGTH_SHORT).show();
if (mReturnResult) {
Intent intent = new Intent();
intent.putExtras(returnData);
setResult(RESULT_OK, intent);
finish();
return;
}
switch (mDecryptTarget) {
case Id.target.message:
String decryptedMessage = returnData
.getString(KeychainIntentService.RESULT_DECRYPTED_STRING);
mMessage.setText(decryptedMessage);
mMessage.setHorizontallyScrolling(false);
mReplyEnabled = false;
// build new action bar
invalidateOptionsMenu();
break;
case Id.target.file:
if (mDeleteAfter.isChecked()) {
// Create and show dialog to delete original file
DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
.newInstance(mInputFilename);
deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
}
break;
default:
// shouldn't happen
break;
}
if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE)) {
String userId = returnData
.getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
mSignatureKeyId = returnData
.getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
mUserIdRest.setText("id: "
+ PgpKeyHelper.convertKeyIdToHex(mSignatureKeyId));
if (userId == null) {
userId = getResources().getString(R.string.unknown_user_id);
}
String chunks[] = userId.split(" <", 2);
userId = chunks[0];
if (chunks.length > 1) {
mUserIdRest.setText("<" + chunks[1]);
}
mUserId.setText(userId);
if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS)) {
mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
} else if (returnData
.getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN)) {
mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
Toast.makeText(DecryptActivity.this,
R.string.unknown_signature_key_touch_to_look_up,
Toast.LENGTH_LONG).show();
} else {
mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
}
mSignatureLayout.setVisibility(View.VISIBLE);
}
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case Id.request.filename: {
if (resultCode == RESULT_OK && data != null) {
try {
String path = FileHelper.getPath(this, data.getData());
Log.d(Constants.TAG, "path=" + path);
mFilename.setText(path);
} catch (NullPointerException e) {
Log.e(Constants.TAG, "Nullpointer while retrieving path!");
}
}
return;
}
// this request is returned after LookupUnknownKeyDialogFragment started
// KeyServerQueryActivity and user looked uo key
case Id.request.look_up_key_id: {
Log.d(Constants.TAG, "Returning from Lookup Key...");
// decrypt again without lookup
mLookupUnknownKey = false;
decryptStart();
return;
}
default: {
break;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
}

View File

@@ -0,0 +1,485 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.remote.RegisteredAppsListActivity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.view.ActionProvider;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.beardedhen.androidbootstrap.FontAwesomeText;
/**
* some fundamental ideas from https://github.com/tobykurien/SherlockNavigationDrawer
*
*
*/
public class DrawerActivity extends SherlockFragmentActivity {
private DrawerLayout mDrawerLayout;
private ListView mDrawerList;
private ActionBarDrawerToggle mDrawerToggle;
private CharSequence mDrawerTitle;
private CharSequence mTitle;
private static Class[] mItemsClass = new Class[] { KeyListPublicActivity.class,
EncryptActivity.class, DecryptActivity.class, ImportKeysActivity.class,
KeyListSecretActivity.class, RegisteredAppsListActivity.class };
private static final int MENU_ID_PREFERENCE = 222;
private static final int MENU_ID_HELP = 223;
protected void setupDrawerNavigation(Bundle savedInstanceState) {
mDrawerTitle = getString(R.string.app_name);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
// set a custom shadow that overlays the main content when the drawer
// opens
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
NavItem mItemIconTexts[] = new NavItem[] {
new NavItem("fa-user", getString(R.string.nav_contacts)),
new NavItem("fa-lock", getString(R.string.nav_encrypt)),
new NavItem("fa-unlock", getString(R.string.nav_decrypt)),
new NavItem("fa-download", getString(R.string.nav_import)),
new NavItem("fa-key", getString(R.string.nav_secret_keys)),
new NavItem("fa-android", getString(R.string.nav_apps)) };
mDrawerList.setAdapter(new NavigationDrawerAdapter(this, R.layout.drawer_list_item,
mItemIconTexts));
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
// enable ActionBar app icon to behave as action to toggle nav drawer
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
// ActionBarDrawerToggle ties together the the proper interactions
// between the sliding drawer and the action bar app icon
mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */
mDrawerLayout, /* DrawerLayout object */
R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */
R.string.drawer_open, /* "open drawer" description for accessibility */
R.string.drawer_close /* "close drawer" description for accessibility */
) {
public void onDrawerClosed(View view) {
getSupportActionBar().setTitle(mTitle);
// creates call to onPrepareOptionsMenu()
supportInvalidateOptionsMenu();
}
public void onDrawerOpened(View drawerView) {
mTitle = getSupportActionBar().getTitle();
getSupportActionBar().setTitle(mDrawerTitle);
// creates call to onPrepareOptionsMenu()
supportInvalidateOptionsMenu();
}
};
mDrawerLayout.setDrawerListener(mDrawerToggle);
// if (savedInstanceState == null) {
// selectItem(0);
// }
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(42, MENU_ID_PREFERENCE, 100, R.string.menu_preferences);
menu.add(42, MENU_ID_HELP, 101, R.string.menu_help);
return super.onCreateOptionsMenu(menu);
}
/* Called whenever we call invalidateOptionsMenu() */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// If the nav drawer is open, hide action items related to the content
// view
boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
// menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// The action bar home/up action should open or close the drawer.
// ActionBarDrawerToggle will take care of this.
if (mDrawerToggle.onOptionsItemSelected(getMenuItem(item))) {
return true;
}
switch (item.getItemId()) {
case MENU_ID_PREFERENCE: {
Intent intent = new Intent(this, PreferencesActivity.class);
startActivity(intent);
return true;
}
case MENU_ID_HELP: {
Intent intent = new Intent(this, HelpActivity.class);
startActivity(intent);
return true;
}
default:
return super.onOptionsItemSelected(item);
}
// Handle action buttons
// switch (item.getItemId()) {
// case R.id.action_websearch:
// // create intent to perform web search for this planet
// Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
// intent.putExtra(SearchManager.QUERY, getSupportActionBar().getTitle());
// // catch event that there's no activity to handle intent
// if (intent.resolveActivity(getPackageManager()) != null) {
// startActivity(intent);
// } else {
// Toast.makeText(this, R.string.app_not_available, Toast.LENGTH_LONG).show();
// }
// return true;
// default:
// return super.onOptionsItemSelected(item);
// }
}
private android.view.MenuItem getMenuItem(final MenuItem item) {
return new android.view.MenuItem() {
@Override
public int getItemId() {
return item.getItemId();
}
public boolean isEnabled() {
return true;
}
@Override
public boolean collapseActionView() {
return false;
}
@Override
public boolean expandActionView() {
return false;
}
@Override
public ActionProvider getActionProvider() {
return null;
}
@Override
public View getActionView() {
return null;
}
@Override
public char getAlphabeticShortcut() {
return 0;
}
@Override
public int getGroupId() {
return 0;
}
@Override
public Drawable getIcon() {
return null;
}
@Override
public Intent getIntent() {
return null;
}
@Override
public ContextMenuInfo getMenuInfo() {
return null;
}
@Override
public char getNumericShortcut() {
return 0;
}
@Override
public int getOrder() {
return 0;
}
@Override
public SubMenu getSubMenu() {
return null;
}
@Override
public CharSequence getTitle() {
return null;
}
@Override
public CharSequence getTitleCondensed() {
return null;
}
@Override
public boolean hasSubMenu() {
return false;
}
@Override
public boolean isActionViewExpanded() {
return false;
}
@Override
public boolean isCheckable() {
return false;
}
@Override
public boolean isChecked() {
return false;
}
@Override
public boolean isVisible() {
return false;
}
@Override
public android.view.MenuItem setActionProvider(ActionProvider actionProvider) {
return null;
}
@Override
public android.view.MenuItem setActionView(View view) {
return null;
}
@Override
public android.view.MenuItem setActionView(int resId) {
return null;
}
@Override
public android.view.MenuItem setAlphabeticShortcut(char alphaChar) {
return null;
}
@Override
public android.view.MenuItem setCheckable(boolean checkable) {
return null;
}
@Override
public android.view.MenuItem setChecked(boolean checked) {
return null;
}
@Override
public android.view.MenuItem setEnabled(boolean enabled) {
return null;
}
@Override
public android.view.MenuItem setIcon(Drawable icon) {
return null;
}
@Override
public android.view.MenuItem setIcon(int iconRes) {
return null;
}
@Override
public android.view.MenuItem setIntent(Intent intent) {
return null;
}
@Override
public android.view.MenuItem setNumericShortcut(char numericChar) {
return null;
}
@Override
public android.view.MenuItem setOnActionExpandListener(OnActionExpandListener listener) {
return null;
}
@Override
public android.view.MenuItem setOnMenuItemClickListener(
OnMenuItemClickListener menuItemClickListener) {
return null;
}
@Override
public android.view.MenuItem setShortcut(char numericChar, char alphaChar) {
return null;
}
@Override
public void setShowAsAction(int actionEnum) {
}
@Override
public android.view.MenuItem setShowAsActionFlags(int actionEnum) {
return null;
}
@Override
public android.view.MenuItem setTitle(CharSequence title) {
return null;
}
@Override
public android.view.MenuItem setTitle(int title) {
return null;
}
@Override
public android.view.MenuItem setTitleCondensed(CharSequence title) {
return null;
}
@Override
public android.view.MenuItem setVisible(boolean visible) {
return null;
}
};
}
/* The click listener for ListView in the navigation drawer */
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectItem(position);
}
}
private void selectItem(int position) {
// update selected item and title, then close the drawer
mDrawerList.setItemChecked(position, true);
// setTitle(mDrawerTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
finish();
overridePendingTransition(0, 0);
Intent intent = new Intent(this, mItemsClass[position]);
startActivity(intent);
// disable animation of activity start
overridePendingTransition(0, 0);
}
/**
* When using the ActionBarDrawerToggle, you must call it during onPostCreate() and
* onConfigurationChanged()...
*/
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
mDrawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Pass any configuration change to the drawer toggles
mDrawerToggle.onConfigurationChanged(newConfig);
}
private class NavItem {
public String icon;
public String title;
public NavItem(String icon, String title) {
super();
this.icon = icon;
this.title = title;
}
}
private class NavigationDrawerAdapter extends ArrayAdapter<NavItem> {
Context context;
int layoutResourceId;
NavItem data[] = null;
public NavigationDrawerAdapter(Context context, int layoutResourceId, NavItem[] data) {
super(context, layoutResourceId, data);
this.layoutResourceId = layoutResourceId;
this.context = context;
this.data = data;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
NavItemHolder holder = null;
if (row == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new NavItemHolder();
holder.img = (FontAwesomeText) row.findViewById(R.id.drawer_item_icon);
holder.txtTitle = (TextView) row.findViewById(R.id.drawer_item_text);
row.setTag(holder);
} else {
holder = (NavItemHolder) row.getTag();
}
NavItem item = data[position];
holder.txtTitle.setText(item.title);
holder.img.setIcon(item.icon);
return row;
}
}
static class NavItemHolder {
FontAwesomeText img;
TextView txtTitle;
}
}

View File

@@ -0,0 +1,666 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import org.sufficientlysecure.keychain.helper.ExportHelper;
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
import org.sufficientlysecure.keychain.ui.widget.KeyEditor;
import org.sufficientlysecure.keychain.ui.widget.SectionView;
import org.sufficientlysecure.keychain.ui.widget.UserIdEditor;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class EditKeyActivity extends SherlockFragmentActivity {
// Actions for internal use only:
public static final String ACTION_CREATE_KEY = Constants.INTENT_PREFIX + "CREATE_KEY";
public static final String ACTION_EDIT_KEY = Constants.INTENT_PREFIX + "EDIT_KEY";
// possible extra keys
public static final String EXTRA_USER_IDS = "user_ids";
public static final String EXTRA_NO_PASSPHRASE = "no_passphrase";
public static final String EXTRA_GENERATE_DEFAULT_KEYS = "generate_default_keys";
// results when saving key
public static final String RESULT_EXTRA_MASTER_KEY_ID = "master_key_id";
public static final String RESULT_EXTRA_USER_ID = "user_id";
// EDIT
private Uri mDataUri;
private PGPSecretKeyRing mKeyRing = null;
private SectionView mUserIdsView;
private SectionView mKeysView;
private String mCurrentPassPhrase = null;
private String mNewPassPhrase = null;
private BootstrapButton mChangePassPhrase;
private CheckBox mNoPassphrase;
Vector<String> mUserIds;
Vector<PGPSecretKey> mKeys;
Vector<Integer> mKeysUsages;
boolean masterCanSign = true;
ExportHelper mExportHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mExportHelper = new ExportHelper(this);
mUserIds = new Vector<String>();
mKeys = new Vector<PGPSecretKey>();
mKeysUsages = new Vector<Integer>();
// Catch Intents opened from other apps
Intent intent = getIntent();
String action = intent.getAction();
if (ACTION_CREATE_KEY.equals(action)) {
handleActionCreateKey(intent);
} else if (ACTION_EDIT_KEY.equals(action)) {
handleActionEditKey(intent);
}
}
/**
* Handle intent action to create new key
*
* @param intent
*/
private void handleActionCreateKey(Intent intent) {
// Inflate a "Done"/"Cancel" custom action bar
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_save,
new View.OnClickListener() {
@Override
public void onClick(View v) {
saveClicked();
}
}, R.string.btn_do_not_save, new View.OnClickListener() {
@Override
public void onClick(View v) {
cancelClicked();
}
});
Bundle extras = intent.getExtras();
mCurrentPassPhrase = "";
if (extras != null) {
// if userId is given, prefill the fields
if (extras.containsKey(EXTRA_USER_IDS)) {
Log.d(Constants.TAG, "UserIds are given!");
mUserIds.add(extras.getString(EXTRA_USER_IDS));
}
// if no passphrase is given
if (extras.containsKey(EXTRA_NO_PASSPHRASE)) {
boolean noPassphrase = extras.getBoolean(EXTRA_NO_PASSPHRASE);
if (noPassphrase) {
// check "no passphrase" checkbox and remove button
mNoPassphrase.setChecked(true);
mChangePassPhrase.setVisibility(View.GONE);
}
}
// generate key
if (extras.containsKey(EXTRA_GENERATE_DEFAULT_KEYS)) {
boolean generateDefaultKeys = extras.getBoolean(EXTRA_GENERATE_DEFAULT_KEYS);
if (generateDefaultKeys) {
// Send all information needed to service generate keys in other thread
Intent serviceIntent = new Intent(this, KeychainIntentService.class);
serviceIntent.setAction(KeychainIntentService.ACTION_GENERATE_DEFAULT_RSA_KEYS);
// fill values for this action
Bundle data = new Bundle();
data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE,
mCurrentPassPhrase);
serviceIntent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after generating is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
this, R.string.progress_generating, ProgressDialog.STYLE_SPINNER) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
// get new key from data bundle returned from service
Bundle data = message.getData();
PGPSecretKeyRing masterKeyRing = (PGPSecretKeyRing) PgpConversionHelper
.BytesToPGPKeyRing(data
.getByteArray(KeychainIntentService.RESULT_NEW_KEY));
PGPSecretKeyRing subKeyRing = (PGPSecretKeyRing) PgpConversionHelper
.BytesToPGPKeyRing(data
.getByteArray(KeychainIntentService.RESULT_NEW_KEY2));
// add master key
@SuppressWarnings("unchecked")
Iterator<PGPSecretKey> masterIt = masterKeyRing.getSecretKeys();
mKeys.add(masterIt.next());
mKeysUsages.add(Id.choice.usage.sign_only);
// add sub key
@SuppressWarnings("unchecked")
Iterator<PGPSecretKey> subIt = subKeyRing.getSecretKeys();
subIt.next(); // masterkey
mKeys.add(subIt.next());
mKeysUsages.add(Id.choice.usage.encrypt_only);
buildLayout();
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
serviceIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
saveHandler.showProgressDialog(this);
// start service with intent
startService(serviceIntent);
}
}
} else {
buildLayout();
}
}
/**
* Handle intent action to edit existing key
*
* @param intent
*/
private void handleActionEditKey(Intent intent) {
// Inflate a "Done"/"Cancel" custom action bar
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.btn_save,
new View.OnClickListener() {
@Override
public void onClick(View v) {
saveClicked();
}
});
mDataUri = intent.getData();
if (mDataUri == null) {
Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
finish();
return;
} else {
Log.d(Constants.TAG, "uri: " + mDataUri);
long keyRingRowId = Long.valueOf(mDataUri.getLastPathSegment());
// get master key id using row id
long masterKeyId = ProviderHelper.getSecretMasterKeyId(this, keyRingRowId);
boolean masterCanSign = ProviderHelper.getSecretMasterKeyCanSign(this, keyRingRowId);
String passphrase = PassphraseCacheService.getCachedPassphrase(this, masterKeyId);
if (passphrase == null) {
showPassphraseDialog(masterKeyId, masterCanSign);
} else {
// PgpMain.setEditPassPhrase(passPhrase);
mCurrentPassPhrase = passphrase;
finallyEdit(masterKeyId, masterCanSign);
}
}
}
private void showPassphraseDialog(final long masterKeyId, final boolean masterCanSign) {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
String passPhrase = PassphraseCacheService.getCachedPassphrase(
EditKeyActivity.this, masterKeyId);
mCurrentPassPhrase = passPhrase;
finallyEdit(masterKeyId, masterCanSign);
} else {
finish();
}
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
try {
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(
EditKeyActivity.this, messenger, masterKeyId);
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
} catch (PgpGeneralException e) {
Log.d(Constants.TAG, "No passphrase for this secret key!");
// send message to handler to start encryption directly
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// show menu only on edit
if (mDataUri != null) {
return super.onPrepareOptionsMenu(menu);
} else {
return false;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getSupportMenuInflater().inflate(R.menu.key_edit, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_key_edit_cancel:
cancelClicked();
return true;
case R.id.menu_key_edit_export_file:
mExportHelper.showExportKeysDialog(mDataUri, Id.type.secret_key, Constants.path.APP_DIR
+ "/secexport.asc");
return true;
case R.id.menu_key_edit_delete: {
// Message is received after key is deleted
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) {
setResult(RESULT_CANCELED);
finish();
}
}
};
mExportHelper.deleteKey(mDataUri, Id.type.secret_key, returnHandler);
return true;
}
}
return super.onOptionsItemSelected(item);
}
@SuppressWarnings("unchecked")
private void finallyEdit(final long masterKeyId, final boolean masterCanSign) {
if (masterKeyId != 0) {
PGPSecretKey masterKey = null;
mKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, masterKeyId);
if (mKeyRing != null) {
masterKey = PgpKeyHelper.getMasterKey(mKeyRing);
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(mKeyRing.getSecretKeys())) {
mKeys.add(key);
mKeysUsages.add(-1); // get usage when view is created
}
} else {
Log.e(Constants.TAG, "Keyring not found with masterKeyId: " + masterKeyId);
Toast.makeText(this, R.string.error_no_secret_key_found, Toast.LENGTH_LONG).show();
}
if (masterKey != null) {
for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
Log.d(Constants.TAG, "Added userId " + userId);
mUserIds.add(userId);
}
}
}
// TODO: ???
if (mCurrentPassPhrase == null) {
mCurrentPassPhrase = "";
}
buildLayout();
if (mCurrentPassPhrase.equals("")) {
// check "no passphrase" checkbox and remove button
mNoPassphrase.setChecked(true);
mChangePassPhrase.setVisibility(View.GONE);
}
}
/**
* Shows the dialog to set a new passphrase
*/
private void showSetPassphraseDialog() {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) {
Bundle data = message.getData();
// set new returned passphrase!
mNewPassPhrase = data
.getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE);
updatePassPhraseButtonText();
}
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
// set title based on isPassphraseSet()
int title = -1;
if (isPassphraseSet()) {
title = R.string.title_change_pass_phrase;
} else {
title = R.string.title_set_passphrase;
}
SetPassphraseDialogFragment setPassphraseDialog = SetPassphraseDialogFragment.newInstance(
messenger, title);
setPassphraseDialog.show(getSupportFragmentManager(), "setPassphraseDialog");
}
/**
* Build layout based on mUserId, mKeys and mKeysUsages Vectors. It creates Views for every user
* id and key.
*/
private void buildLayout() {
setContentView(R.layout.edit_key_activity);
// find views
mChangePassPhrase = (BootstrapButton) findViewById(R.id.edit_key_btn_change_pass_phrase);
mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase);
// Build layout based on given userIds and keys
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout container = (LinearLayout) findViewById(R.id.edit_key_container);
mUserIdsView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
mUserIdsView.setType(Id.type.user_id);
mUserIdsView.setCanEdit(masterCanSign);
mUserIdsView.setUserIds(mUserIds);
container.addView(mUserIdsView);
mKeysView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
mKeysView.setType(Id.type.key);
mKeysView.setCanEdit(masterCanSign);
mKeysView.setKeys(mKeys, mKeysUsages);
container.addView(mKeysView);
updatePassPhraseButtonText();
mChangePassPhrase.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
showSetPassphraseDialog();
}
});
// disable passphrase when no passphrase checkobox is checked!
mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
// remove passphrase
mNewPassPhrase = null;
mChangePassPhrase.setVisibility(View.GONE);
} else {
mChangePassPhrase.setVisibility(View.VISIBLE);
}
}
});
}
private long getMasterKeyId() {
if (mKeysView.getEditors().getChildCount() == 0) {
return 0;
}
return ((KeyEditor) mKeysView.getEditors().getChildAt(0)).getValue().getKeyID();
}
public boolean isPassphraseSet() {
if (mNoPassphrase.isChecked()) {
return true;
} else if ((!mCurrentPassPhrase.equals(""))
|| (mNewPassPhrase != null && !mNewPassPhrase.equals(""))) {
return true;
} else {
return false;
}
}
private void saveClicked() {
try {
if (!isPassphraseSet()) {
throw new PgpGeneralException(this.getString(R.string.set_a_passphrase));
}
// Send all information needed to service to edit key in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING);
// fill values for this action
Bundle data = new Bundle();
data.putString(KeychainIntentService.SAVE_KEYRING_CURRENT_PASSPHRASE,
mCurrentPassPhrase);
data.putString(KeychainIntentService.SAVE_KEYRING_NEW_PASSPHRASE, mNewPassPhrase);
data.putStringArrayList(KeychainIntentService.SAVE_KEYRING_USER_IDS,
getUserIds(mUserIdsView));
ArrayList<PGPSecretKey> keys = getKeys(mKeysView);
data.putByteArray(KeychainIntentService.SAVE_KEYRING_KEYS,
PgpConversionHelper.PGPSecretKeyArrayListToBytes(keys));
data.putIntegerArrayList(KeychainIntentService.SAVE_KEYRING_KEYS_USAGES,
getKeysUsages(mKeysView));
data.putLong(KeychainIntentService.SAVE_KEYRING_MASTER_KEY_ID, getMasterKeyId());
data.putBoolean(KeychainIntentService.SAVE_KEYRING_CAN_SIGN, masterCanSign);
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after saving is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
R.string.progress_saving, ProgressDialog.STYLE_HORIZONTAL) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
Intent data = new Intent();
data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, getMasterKeyId());
ArrayList<String> userIds = null;
try {
userIds = getUserIds(mUserIdsView);
} catch (PgpGeneralException e) {
Log.e(Constants.TAG, "exception while getting user ids", e);
}
data.putExtra(RESULT_EXTRA_USER_ID, userIds.get(0));
setResult(RESULT_OK, data);
finish();
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
} catch (PgpGeneralException e) {
Toast.makeText(this, getString(R.string.error_message, e.getMessage()),
Toast.LENGTH_SHORT).show();
}
}
private void cancelClicked() {
setResult(RESULT_CANCELED);
finish();
}
/**
* Returns user ids from the SectionView
*
* @param userIdsView
* @return
*/
private ArrayList<String> getUserIds(SectionView userIdsView) throws PgpGeneralException {
ArrayList<String> userIds = new ArrayList<String>();
ViewGroup userIdEditors = userIdsView.getEditors();
boolean gotMainUserId = false;
for (int i = 0; i < userIdEditors.getChildCount(); ++i) {
UserIdEditor editor = (UserIdEditor) userIdEditors.getChildAt(i);
String userId = null;
try {
userId = editor.getValue();
} catch (UserIdEditor.NoNameException e) {
throw new PgpGeneralException(this.getString(R.string.error_user_id_needs_a_name));
} catch (UserIdEditor.NoEmailException e) {
throw new PgpGeneralException(
this.getString(R.string.error_user_id_needs_an_email_address));
} catch (UserIdEditor.InvalidEmailException e) {
throw new PgpGeneralException(e.getMessage());
}
if (userId.equals("")) {
continue;
}
if (editor.isMainUserId()) {
userIds.add(0, userId);
gotMainUserId = true;
} else {
userIds.add(userId);
}
}
if (userIds.size() == 0) {
throw new PgpGeneralException(getString(R.string.error_key_needs_a_user_id));
}
if (!gotMainUserId) {
throw new PgpGeneralException(getString(R.string.error_main_user_id_must_not_be_empty));
}
return userIds;
}
/**
* Returns keys from the SectionView
*
* @param keysView
* @return
*/
private ArrayList<PGPSecretKey> getKeys(SectionView keysView) throws PgpGeneralException {
ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
ViewGroup keyEditors = keysView.getEditors();
if (keyEditors.getChildCount() == 0) {
throw new PgpGeneralException(getString(R.string.error_key_needs_master_key));
}
for (int i = 0; i < keyEditors.getChildCount(); ++i) {
KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
keys.add(editor.getValue());
}
return keys;
}
/**
* Returns usage selections of keys from the SectionView
*
* @param keysView
* @return
*/
private ArrayList<Integer> getKeysUsages(SectionView keysView) throws PgpGeneralException {
ArrayList<Integer> getKeysUsages = new ArrayList<Integer>();
ViewGroup keyEditors = keysView.getEditors();
if (keyEditors.getChildCount() == 0) {
throw new PgpGeneralException(getString(R.string.error_key_needs_master_key));
}
for (int i = 0; i < keyEditors.getChildCount(); ++i) {
KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
getKeysUsages.add(editor.getUsage());
}
return getKeysUsages;
}
private void updatePassPhraseButtonText() {
mChangePassPhrase.setText(isPassphraseSet() ? getString(R.string.btn_change_passphrase)
: getString(R.string.btn_set_passphrase));
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.ArrayList;
import org.sufficientlysecure.keychain.R;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import android.widget.TextView;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.Tab;
import com.actionbarsherlock.app.SherlockFragmentActivity;
public class HelpActivity extends SherlockFragmentActivity {
public static final String EXTRA_SELECTED_TAB = "selectedTab";
ViewPager mViewPager;
TabsAdapter mTabsAdapter;
TextView tabCenter;
TextView tabText;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.help_activity);
mViewPager = (ViewPager) findViewById(R.id.pager);
final ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setHomeButtonEnabled(false);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
mTabsAdapter = new TabsAdapter(this, mViewPager);
int selectedTab = 0;
Intent intent = getIntent();
if (intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_SELECTED_TAB)) {
selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB);
}
Bundle startBundle = new Bundle();
startBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_start);
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_start)),
HelpFragmentHtml.class, startBundle, (selectedTab == 0 ? true : false));
Bundle nfcBundle = new Bundle();
nfcBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_nfc_beam);
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_nfc_beam)),
HelpFragmentHtml.class, nfcBundle, (selectedTab == 1 ? true : false));
Bundle changelogBundle = new Bundle();
changelogBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_changelog);
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_changelog)),
HelpFragmentHtml.class, changelogBundle, (selectedTab == 2 ? true : false));
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_about)),
HelpFragmentAbout.class, null, (selectedTab == 3 ? true : false));
}
public static class TabsAdapter extends FragmentPagerAdapter implements ActionBar.TabListener,
ViewPager.OnPageChangeListener {
private final Context mContext;
private final ActionBar mActionBar;
private final ViewPager mViewPager;
private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
static final class TabInfo {
private final Class<?> clss;
private final Bundle args;
TabInfo(Class<?> _class, Bundle _args) {
clss = _class;
args = _args;
}
}
public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) {
super(activity.getSupportFragmentManager());
mContext = activity;
mActionBar = activity.getSupportActionBar();
mViewPager = pager;
mViewPager.setAdapter(this);
mViewPager.setOnPageChangeListener(this);
}
public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args, boolean selected) {
TabInfo info = new TabInfo(clss, args);
tab.setTag(info);
tab.setTabListener(this);
mTabs.add(info);
mActionBar.addTab(tab, selected);
notifyDataSetChanged();
}
@Override
public int getCount() {
return mTabs.size();
}
@Override
public Fragment getItem(int position) {
TabInfo info = mTabs.get(position);
return Fragment.instantiate(mContext, info.clss.getName(), info.args);
}
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
public void onPageSelected(int position) {
mActionBar.setSelectedNavigationItem(position);
}
public void onPageScrollStateChanged(int state) {
}
public void onTabSelected(Tab tab, FragmentTransaction ft) {
Object tag = tab.getTag();
for (int i = 0; i < mTabs.size(); i++) {
if (mTabs.get(i) == tag) {
mViewPager.setCurrentItem(i);
}
}
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
}
public void onTabReselected(Tab tab, FragmentTransaction ft) {
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.htmltextview.HtmlTextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.Log;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.actionbarsherlock.app.SherlockFragment;
public class HelpFragmentAbout extends SherlockFragment {
/**
* Workaround for Android Bug. See
* http://stackoverflow.com/questions/8748064/starting-activity-from
* -fragment-causes-nullpointerexception
*/
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
setUserVisibleHint(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.help_about_fragment, container, false);
TextView versionText = (TextView) view.findViewById(R.id.help_about_version);
versionText.setText(getString(R.string.help_about_version) + " " + getVersion());
HtmlTextView aboutTextView = (HtmlTextView) view.findViewById(R.id.help_about_text);
// load html from raw resource (Parsing handled by HtmlTextView library)
aboutTextView.setHtmlFromRawResource(getActivity(), R.raw.help_about);
// no flickering when clicking textview for Android < 4
aboutTextView.setTextColor(getResources().getColor(android.R.color.black));
return view;
}
/**
* Get the current package version.
*
* @return The current version.
*/
private String getVersion() {
String result = "";
try {
PackageManager manager = getActivity().getPackageManager();
PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0);
result = String.format("%s (%s)", info.versionName, info.versionCode);
} catch (NameNotFoundException e) {
Log.w(Constants.TAG, "Unable to get application version: " + e.getMessage());
result = "Unable to get application version.";
}
return result;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.htmltextview.HtmlTextView;
import android.app.Activity;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
import com.actionbarsherlock.app.SherlockFragment;
public class HelpFragmentHtml extends SherlockFragment {
private Activity mActivity;
private int htmlFile;
public static final String ARG_HTML_FILE = "htmlFile";
/**
* Create a new instance of HelpFragmentHtml, providing "htmlFile" as an argument.
*/
static HelpFragmentHtml newInstance(int htmlFile) {
HelpFragmentHtml f = new HelpFragmentHtml();
// Supply html raw file input as an argument.
Bundle args = new Bundle();
args.putInt(ARG_HTML_FILE, htmlFile);
f.setArguments(args);
return f;
}
/**
* Workaround for Android Bug. See
* http://stackoverflow.com/questions/8748064/starting-activity-from
* -fragment-causes-nullpointerexception
*/
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
setUserVisibleHint(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mActivity = getActivity();
htmlFile = getArguments().getInt(ARG_HTML_FILE);
ScrollView scroller = new ScrollView(mActivity);
HtmlTextView text = new HtmlTextView(mActivity);
// padding
int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, mActivity
.getResources().getDisplayMetrics());
text.setPadding(padding, padding, padding, 0);
scroller.addView(text);
// load html from raw resource (Parsing handled by HtmlTextView library)
text.setHtmlFromRawResource(getActivity(), htmlFile);
// no flickering when clicking textview for Android < 4
text.setTextColor(getResources().getColor(android.R.color.black));
return scroller;
}
}

View File

@@ -0,0 +1,468 @@
/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2011 Senecaso
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.ArrayList;
import java.util.List;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class ImportKeysActivity extends DrawerActivity implements OnNavigationListener {
public static final String ACTION_IMPORT_KEY = Constants.INTENT_PREFIX + "IMPORT_KEY";
public static final String ACTION_IMPORT_KEY_FROM_QR_CODE = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_QR_CODE";
// Actions for internal use only:
public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_FILE";
public static final String ACTION_IMPORT_KEY_FROM_NFC = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_NFC";
// only used by IMPORT
public static final String EXTRA_KEY_BYTES = "key_bytes";
// TODO: import keys from server
// public static final String EXTRA_KEY_ID = "keyId";
protected boolean mDeleteAfterImport = false;
FileDialogFragment mFileDialog;
ImportKeysListFragment mListFragment;
OnNavigationListener mOnNavigationListener;
String[] mNavigationStrings;
Fragment mCurrentFragment;
BootstrapButton mImportButton;
// BootstrapButton mImportSignUploadButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.import_keys_activity);
mImportButton = (BootstrapButton) findViewById(R.id.import_import);
mImportButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
importKeys();
}
});
// mImportSignUploadButton = (BootstrapButton) findViewById(R.id.import_sign_and_upload);
// mImportSignUploadButton.setOnClickListener(new OnClickListener() {
// @Override
// public void onClick(View v) {
// signAndUploadOnClick();
// }
// });
getSupportActionBar().setDisplayShowTitleEnabled(false);
setupDrawerNavigation(savedInstanceState);
// set actionbar without home button if called from another app
// ActionBarHelper.setBackButton(this);
// set drop down navigation
mNavigationStrings = getResources().getStringArray(R.array.import_action_list);
Context context = getSupportActionBar().getThemedContext();
ArrayAdapter<CharSequence> list = ArrayAdapter.createFromResource(context,
R.array.import_action_list, R.layout.sherlock_spinner_item);
list.setDropDownViewResource(R.layout.sherlock_spinner_dropdown_item);
getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
getSupportActionBar().setListNavigationCallbacks(list, this);
handleActions(savedInstanceState, getIntent());
}
protected void handleActions(Bundle savedInstanceState, Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
if (extras == null) {
extras = new Bundle();
}
/**
* Android Standard Actions
*/
if (Intent.ACTION_VIEW.equals(action)) {
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
// override action to delegate it to Keychain's ACTION_IMPORT_KEY
action = ACTION_IMPORT_KEY;
}
/**
* Keychain's own Actions
*/
if (ACTION_IMPORT_KEY.equals(action)) {
if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
String importFilename = intent.getData().getPath();
// display selected filename
getSupportActionBar().setSelectedNavigationItem(1);
Bundle args = new Bundle();
args.putString(ImportKeysFileFragment.ARG_PATH, importFilename);
loadFragment(ImportKeysFileFragment.class, args, mNavigationStrings[1]);
// directly load data
startListFragment(savedInstanceState, null, importFilename);
} else if (extras.containsKey(EXTRA_KEY_BYTES)) {
byte[] importData = intent.getByteArrayExtra(EXTRA_KEY_BYTES);
// directly load data
startListFragment(savedInstanceState, importData, null);
}
} else {
// Internal actions
startListFragment(savedInstanceState, null, null);
if (ACTION_IMPORT_KEY_FROM_FILE.equals(action)) {
getSupportActionBar().setSelectedNavigationItem(1);
loadFragment(ImportKeysFileFragment.class, null, mNavigationStrings[1]);
} else if (ACTION_IMPORT_KEY_FROM_QR_CODE.equals(action)) {
getSupportActionBar().setSelectedNavigationItem(2);
loadFragment(ImportKeysQrCodeFragment.class, null, mNavigationStrings[2]);
} else if (ACTION_IMPORT_KEY_FROM_NFC.equals(action)) {
getSupportActionBar().setSelectedNavigationItem(3);
loadFragment(ImportKeysNFCFragment.class, null, mNavigationStrings[3]);
}
}
}
private void startListFragment(Bundle savedInstanceState, byte[] bytes, String filename) {
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.import_keys_list_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of the fragment
mListFragment = ImportKeysListFragment.newInstance(bytes, filename);
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
getSupportFragmentManager().beginTransaction()
.replace(R.id.import_keys_list_container, mListFragment)
.commitAllowingStateLoss();
// do it immediately!
getSupportFragmentManager().executePendingTransactions();
}
}
@Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
// Create new fragment from our own Fragment class
switch (itemPosition) {
case 0:
loadFragment(ImportKeysServerFragment.class, null, mNavigationStrings[itemPosition]);
break;
case 1:
loadFragment(ImportKeysFileFragment.class, null, mNavigationStrings[itemPosition]);
break;
case 2:
loadFragment(ImportKeysQrCodeFragment.class, null, mNavigationStrings[itemPosition]);
break;
case 3:
loadFragment(ImportKeysClipboardFragment.class, null, mNavigationStrings[itemPosition]);
break;
case 4:
loadFragment(ImportKeysNFCFragment.class, null, mNavigationStrings[itemPosition]);
break;
default:
break;
}
return true;
}
private void loadFragment(Class<?> clss, Bundle args, String tag) {
mCurrentFragment = Fragment.instantiate(this, clss.getName(), args);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
// Replace whatever is in the fragment container with this fragment
// and give the fragment a tag name equal to the string at the position selected
ft.replace(R.id.import_navigation_fragment, mCurrentFragment, tag);
// Apply changes
ft.commit();
}
public void loadCallback(byte[] importData, String importFilename) {
mListFragment.loadNew(importData, importFilename);
}
// private void importAndSignOld(final long keyId, final String expectedFingerprint) {
// if (expectedFingerprint != null && expectedFingerprint.length() > 0) {
//
// Thread t = new Thread() {
// @Override
// public void run() {
// try {
// // TODO: display some sort of spinner here while the user waits
//
// // TODO: there should be only 1
// HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]);
// String encodedKey = server.get(keyId);
//
// PGPKeyRing keyring = PGPHelper.decodeKeyRing(new ByteArrayInputStream(
// encodedKey.getBytes()));
// if (keyring != null && keyring instanceof PGPPublicKeyRing) {
// PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
//
// // make sure the fingerprints match before we cache this thing
// String actualFingerprint = PGPHelper.convertFingerprintToHex(publicKeyRing
// .getPublicKey().getFingerprint());
// if (expectedFingerprint.equals(actualFingerprint)) {
// // store the signed key in our local cache
// int retval = PGPMain.storeKeyRingInCache(publicKeyRing);
// if (retval != Id.return_value.ok
// && retval != Id.return_value.updated) {
// status.putString(EXTRA_ERROR,
// "Failed to store signed key in local cache");
// } else {
// Intent intent = new Intent(ImportFromQRCodeActivity.this,
// SignKeyActivity.class);
// intent.putExtra(EXTRA_KEY_ID, keyId);
// startActivityForResult(intent, Id.request.sign_key);
// }
// } else {
// status.putString(
// EXTRA_ERROR,
// "Scanned fingerprint does NOT match the fingerprint of the received key. You shouldnt trust this key.");
// }
// }
// } catch (QueryException e) {
// Log.e(TAG, "Failed to query KeyServer", e);
// status.putString(EXTRA_ERROR, "Failed to query KeyServer");
// status.putInt(Constants.extras.STATUS, Id.message.done);
// } catch (IOException e) {
// Log.e(TAG, "Failed to query KeyServer", e);
// status.putString(EXTRA_ERROR, "Failed to query KeyServer");
// status.putInt(Constants.extras.STATUS, Id.message.done);
// }
// }
// };
//
// t.setName("KeyExchange Download Thread");
// t.setDaemon(true);
// t.start();
// }
// }
/**
* Import keys with mImportData
*/
public void importKeys() {
if (mListFragment.getKeyBytes() != null || mListFragment.getImportFilename() != null) {
Log.d(Constants.TAG, "importKeys started");
// Send all information needed to service to import key in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING);
// fill values for this action
Bundle data = new Bundle();
// get selected key ids
List<ImportKeysListEntry> listEntries = mListFragment.getData();
ArrayList<Long> selectedKeyIds = new ArrayList<Long>();
for (ImportKeysListEntry entry : listEntries) {
if (entry.isSelected()) {
selectedKeyIds.add(entry.keyId);
}
}
data.putSerializable(KeychainIntentService.IMPORT_KEY_LIST, selectedKeyIds);
if (mListFragment.getKeyBytes() != null) {
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
data.putByteArray(KeychainIntentService.IMPORT_BYTES, mListFragment.getKeyBytes());
} else {
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
data.putString(KeychainIntentService.IMPORT_FILENAME,
mListFragment.getImportFilename());
}
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after importing is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
R.string.progress_importing, ProgressDialog.STYLE_HORIZONTAL) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
// get returned data bundle
Bundle returnData = message.getData();
int added = returnData.getInt(KeychainIntentService.RESULT_IMPORT_ADDED);
int updated = returnData
.getInt(KeychainIntentService.RESULT_IMPORT_UPDATED);
int bad = returnData.getInt(KeychainIntentService.RESULT_IMPORT_BAD);
String toastMessage;
if (added > 0 && updated > 0) {
String addedStr = getResources().getQuantityString(
R.plurals.keys_added_and_updated_1, added, added);
String updatedStr = getResources().getQuantityString(
R.plurals.keys_added_and_updated_2, updated, updated);
toastMessage = addedStr + updatedStr;
} else if (added > 0) {
toastMessage = getResources().getQuantityString(R.plurals.keys_added,
added, added);
} else if (updated > 0) {
toastMessage = getResources().getQuantityString(R.plurals.keys_updated,
updated, updated);
} else {
toastMessage = getString(R.string.no_keys_added_or_updated);
}
Toast.makeText(ImportKeysActivity.this, toastMessage, Toast.LENGTH_SHORT)
.show();
if (bad > 0) {
AlertDialog.Builder alert = new AlertDialog.Builder(
ImportKeysActivity.this);
alert.setIcon(android.R.drawable.ic_dialog_alert);
alert.setTitle(R.string.warning);
alert.setMessage(ImportKeysActivity.this.getResources()
.getQuantityString(R.plurals.bad_keys_encountered, bad, bad));
alert.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
alert.setCancelable(true);
alert.create().show();
} else if (mDeleteAfterImport) {
// everything went well, so now delete, if that was turned on
DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
.newInstance(mListFragment.getImportFilename());
deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
}
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
} else {
Toast.makeText(this, R.string.error_nothing_import, Toast.LENGTH_LONG).show();
}
}
public void importOnClick() {
importKeys();
}
// public void signAndUploadOnClick() {
// // first, import!
// // importOnClick(view);
//
// // TODO: implement sign and upload!
// Toast.makeText(ImportKeysActivity.this, "Not implemented right now!", Toast.LENGTH_SHORT)
// .show();
// }
/**
* NFC
*/
@Override
public void onResume() {
super.onResume();
// Check to see that the Activity started due to an Android Beam
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
handleActionNdefDiscovered(getIntent());
}
}
/**
* NFC
*/
@Override
public void onNewIntent(Intent intent) {
// onResume gets called after this to handle the intent
setIntent(intent);
}
/**
* NFC: Parses the NDEF Message from the intent and prints to the TextView
*/
@SuppressLint("NewApi")
void handleActionNdefDiscovered(Intent intent) {
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
// only one message sent during the beam
NdefMessage msg = (NdefMessage) rawMsgs[0];
// record 0 contains the MIME type, record 1 is the AAR, if present
byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload();
Intent importIntent = new Intent(this, ImportKeysActivity.class);
importIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY);
importIntent.putExtra(ImportKeysActivity.EXTRA_KEY_BYTES, receivedKeyringBytes);
handleActions(null, importIntent);
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class ImportKeysClipboardFragment extends Fragment {
private ImportKeysActivity mImportActivity;
private BootstrapButton mButton;
/**
* Creates new instance of this fragment
*/
public static ImportKeysClipboardFragment newInstance() {
ImportKeysClipboardFragment frag = new ImportKeysClipboardFragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.import_keys_clipboard_fragment, container, false);
mButton = (BootstrapButton) view.findViewById(R.id.import_clipboard_button);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity());
String sendText = "";
if (clipboardText != null)
sendText = clipboardText.toString();
mImportActivity.loadCallback(sendText.getBytes(), null);
}
});
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mImportActivity = (ImportKeysActivity) getActivity();
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.FileHelper;
import org.sufficientlysecure.keychain.util.Log;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class ImportKeysFileFragment extends Fragment {
public static final String ARG_PATH = "path";
private ImportKeysActivity mImportActivity;
private EditText mFilename;
private BootstrapButton mBrowse;
/**
* Creates new instance of this fragment
*/
public static ImportKeysFileFragment newInstance(String path) {
ImportKeysFileFragment frag = new ImportKeysFileFragment();
Bundle args = new Bundle();
args.putString(ARG_PATH, path);
frag.setArguments(args);
return frag;
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.import_keys_file_fragment, container, false);
mFilename = (EditText) view.findViewById(R.id.import_keys_file_input);
mBrowse = (BootstrapButton) view.findViewById(R.id.import_keys_file_browse);
mBrowse.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// open .asc or .gpg files
// setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
// or gpg types!
FileHelper.openFile(ImportKeysFileFragment.this, mFilename.getText().toString(),
"*/*", Id.request.filename);
}
});
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mImportActivity = (ImportKeysActivity) getActivity();
// set default path
String path = Constants.path.APP_DIR + "/";
if (getArguments() != null && getArguments().containsKey(ARG_PATH)) {
path = getArguments().getString(ARG_PATH);
}
mFilename.setText(path);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode & 0xFFFF) {
case Id.request.filename: {
if (resultCode == Activity.RESULT_OK && data != null) {
String path = null;
try {
path = data.getData().getPath();
Log.d(Constants.TAG, "path=" + path);
// set filename to edittext
mFilename.setText(path);
} catch (NullPointerException e) {
Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
}
// load data
mImportActivity.loadCallback(null, path);
}
break;
}
default:
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.List;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.ListView;
import com.actionbarsherlock.app.SherlockListFragment;
public class ImportKeysListFragment extends SherlockListFragment implements
LoaderManager.LoaderCallbacks<List<ImportKeysListEntry>> {
private static final String ARG_FILENAME = "filename";
private static final String ARG_BYTES = "bytes";
private Activity mActivity;
private ImportKeysAdapter mAdapter;
private byte[] mKeyBytes;
private String mImportFilename;
public byte[] getKeyBytes() {
return mKeyBytes;
}
public String getImportFilename() {
return mImportFilename;
}
public List<ImportKeysListEntry> getData() {
return mAdapter.getData();
}
/**
* Creates new instance of this fragment
*/
public static ImportKeysListFragment newInstance(byte[] bytes, String filename) {
ImportKeysListFragment frag = new ImportKeysListFragment();
Bundle args = new Bundle();
args.putByteArray(ARG_BYTES, bytes);
args.putString(ARG_FILENAME, filename);
frag.setArguments(args);
return frag;
}
/**
* Define Adapter and Loader on create of Activity
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mActivity = getActivity();
if (getArguments() != null) {
mImportFilename = getArguments().getString(ARG_FILENAME);
mKeyBytes = getArguments().getByteArray(ARG_BYTES);
}
// Give some text to display if there is no data. In a real
// application this would come from a resource.
setEmptyText(mActivity.getString(R.string.error_nothing_import));
// Create an empty adapter we will use to display the loaded data.
mAdapter = new ImportKeysAdapter(mActivity);
setListAdapter(mAdapter);
// Start out with a progress indicator.
setListShown(false);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
// give arguments to onCreateLoader()
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
// Select checkbox!
// Update underlying data and notify adapter of change. The adapter will
// update the view automatically.
ImportKeysListEntry entry = mAdapter.getItem(position);
entry.setSelected(!entry.isSelected());
mAdapter.notifyDataSetChanged();
}
public void loadNew(byte[] importData, String importFilename) {
this.mKeyBytes = importData;
this.mImportFilename = importFilename;
getLoaderManager().restartLoader(0, null, this);
}
@Override
public Loader<List<ImportKeysListEntry>> onCreateLoader(int id, Bundle args) {
InputData inputData = getInputData(mKeyBytes, mImportFilename);
return new ImportKeysListLoader(mActivity, inputData);
}
private InputData getInputData(byte[] importBytes, String importFilename) {
InputData inputData = null;
if (importBytes != null) {
inputData = new InputData(new ByteArrayInputStream(importBytes), importBytes.length);
} else if (importFilename != null) {
try {
inputData = new InputData(new FileInputStream(importFilename),
importFilename.length());
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Failed to init FileInputStream!", e);
}
}
return inputData;
}
@Override
public void onLoadFinished(Loader<List<ImportKeysListEntry>> loader,
List<ImportKeysListEntry> data) {
Log.d(Constants.TAG, "data: " + data);
// swap in the real data!
mAdapter.setData(data);
mAdapter.notifyDataSetChanged();
setListAdapter(mAdapter);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
@Override
public void onLoaderReset(Loader<List<ImportKeysListEntry>> loader) {
// Clear the data in the adapter.
mAdapter.clear();
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.R;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class ImportKeysNFCFragment extends Fragment {
private BootstrapButton mButton;
/**
* Creates new instance of this fragment
*/
public static ImportKeysNFCFragment newInstance() {
ImportKeysNFCFragment frag = new ImportKeysNFCFragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.import_keys_nfc_fragment, container, false);
mButton = (BootstrapButton) view.findViewById(R.id.import_nfc_button);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// show nfc help
Intent intent = new Intent(getActivity(), HelpActivity.class);
intent.putExtra(HelpActivity.EXTRA_SELECTED_TAB, 1);
startActivityForResult(intent, 0);
}
});
return view;
}
}

View File

@@ -0,0 +1,209 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.ArrayList;
import java.util.Locale;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.beardedhen.androidbootstrap.BootstrapButton;
import com.google.zxing.integration.android.IntentResult;
public class ImportKeysQrCodeFragment extends Fragment {
private ImportKeysActivity mImportActivity;
private BootstrapButton mButton;
private TextView mText;
private ProgressBar mProgress;
private String[] mScannedContent;
/**
* Creates new instance of this fragment
*/
public static ImportKeysQrCodeFragment newInstance() {
ImportKeysQrCodeFragment frag = new ImportKeysQrCodeFragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.import_keys_qr_code_fragment, container, false);
mButton = (BootstrapButton) view.findViewById(R.id.import_qrcode_button);
mText = (TextView) view.findViewById(R.id.import_qrcode_text);
mProgress = (ProgressBar) view.findViewById(R.id.import_qrcode_progress);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// scan using xzing's Barcode Scanner
new IntentIntegratorSupportV4(ImportKeysQrCodeFragment.this).initiateScan();
}
});
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mImportActivity = (ImportKeysActivity) getActivity();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode & 0xFFFF) {
case IntentIntegratorSupportV4.REQUEST_CODE: {
IntentResult scanResult = IntentIntegratorSupportV4.parseActivityResult(requestCode,
resultCode, data);
if (scanResult != null && scanResult.getFormatName() != null) {
Log.d(Constants.TAG, "scanResult content: " + scanResult.getContents());
// look if it's fingerprint only
if (scanResult.getContents().toLowerCase(Locale.ENGLISH).startsWith("openpgp4fpr")) {
importFingerprint(scanResult.getContents().toLowerCase(Locale.ENGLISH));
return;
}
// look if it is the whole key
String[] parts = scanResult.getContents().split(",");
if (parts.length == 3) {
importParts(parts);
return;
}
// fail...
Toast.makeText(getActivity(), R.string.import_qr_code_wrong, Toast.LENGTH_LONG)
.show();
}
break;
}
default:
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
private void importFingerprint(String uri) {
String fingerprint = uri.split(":")[1];
Log.d(Constants.TAG, "fingerprint: " + fingerprint);
if (fingerprint.length() < 16) {
Toast.makeText(getActivity(), R.string.import_qr_code_too_short_fingerprint,
Toast.LENGTH_LONG).show();
return;
}
Intent queryIntent = new Intent(getActivity(), KeyServerQueryActivity.class);
queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
queryIntent.putExtra(KeyServerQueryActivity.EXTRA_FINGERPRINT, fingerprint);
startActivity(queryIntent);
}
private void importParts(String[] parts) {
int counter = Integer.valueOf(parts[0]);
int size = Integer.valueOf(parts[1]);
String content = parts[2];
Log.d(Constants.TAG, "" + counter);
Log.d(Constants.TAG, "" + size);
Log.d(Constants.TAG, "" + content);
// first qr code -> setup
if (counter == 0) {
mScannedContent = new String[size];
mProgress.setMax(size);
mProgress.setVisibility(View.VISIBLE);
mText.setVisibility(View.VISIBLE);
}
if (mScannedContent == null || counter > mScannedContent.length) {
Toast.makeText(getActivity(), R.string.import_qr_code_start_with_one, Toast.LENGTH_LONG)
.show();
return;
}
// save scanned content
mScannedContent[counter] = content;
// get missing numbers
ArrayList<Integer> missing = new ArrayList<Integer>();
for (int i = 0; i < mScannedContent.length; i++) {
if (mScannedContent[i] == null) {
missing.add(i);
}
}
// update progress and text
int alreadyScanned = mScannedContent.length - missing.size();
mProgress.setProgress(alreadyScanned);
String missingString = "";
for (int m : missing) {
if (!missingString.equals("")) {
missingString += ", ";
}
missingString += String.valueOf(m + 1);
}
String missingText = getResources().getQuantityString(R.plurals.import_qr_code_missing,
missing.size(), missingString);
mText.setText(missingText);
// finished!
if (missing.size() == 0) {
mText.setText(R.string.import_qr_code_finished);
String result = "";
for (String in : mScannedContent) {
result += in;
}
mImportActivity.loadCallback(result.getBytes(), null);
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.R;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class ImportKeysServerFragment extends Fragment {
private BootstrapButton mButton;
/**
* Creates new instance of this fragment
*/
public static ImportKeysServerFragment newInstance() {
ImportKeysServerFragment frag = new ImportKeysServerFragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.import_keys_keyserver_fragment, container, false);
mButton = (BootstrapButton) view.findViewById(R.id.import_keyserver_button);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO: use fragment instead of activity, handle onresult here!
startActivityForResult(new Intent(getActivity(), KeyServerQueryActivity.class), 0);
}
});
return view;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ExportHelper;
import android.content.Intent;
import android.os.Bundle;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
public class KeyListPublicActivity extends DrawerActivity {
ExportHelper mExportHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mExportHelper = new ExportHelper(this);
setContentView(R.layout.key_list_public_activity);
// now setup navigation drawer in DrawerActivity...
setupDrawerNavigation(savedInstanceState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getSupportMenuInflater().inflate(R.menu.key_list_public, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_key_list_public_import:
Intent intentImport = new Intent(this, ImportKeysActivity.class);
startActivityForResult(intentImport, 0);
return true;
case R.id.menu_key_list_public_export:
mExportHelper.showExportKeysDialog(null, Id.type.public_key, Constants.path.APP_DIR
+ "/pubexport.asc");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

View File

@@ -0,0 +1,299 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.Set;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.KeyListPublicAdapter;
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
import se.emilsjolander.stickylistheaders.ApiLevelTooLowException;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.ListView;
import com.beardedhen.androidbootstrap.BootstrapButton;
/**
* Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
* StickyListHeaders library which does not extend upon ListView.
*/
public class KeyListPublicFragment extends Fragment implements AdapterView.OnItemClickListener,
LoaderManager.LoaderCallbacks<Cursor> {
private KeyListPublicAdapter mAdapter;
private StickyListHeadersListView mStickyList;
// empty layout
private BootstrapButton mButtonEmptyCreate;
private BootstrapButton mButtonEmptyImport;
/**
* Load custom layout with StickyListView from library
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.key_list_public_fragment, container, false);
mButtonEmptyCreate = (BootstrapButton) view.findViewById(R.id.key_list_empty_button_create);
mButtonEmptyCreate.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getActivity(), EditKeyActivity.class);
intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true);
intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view
startActivityForResult(intent, 0);
}
});
mButtonEmptyImport = (BootstrapButton) view.findViewById(R.id.key_list_empty_button_import);
mButtonEmptyImport.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intentImportFromFile = new Intent(getActivity(), ImportKeysActivity.class);
intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE);
startActivityForResult(intentImportFromFile, 0);
}
});
return view;
}
/**
* Define Adapter and Loader on create of Activity
*/
@SuppressLint("NewApi")
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// mKeyListPublicActivity = (KeyListPublicActivity) getActivity();
mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list);
mStickyList.setOnItemClickListener(this);
mStickyList.setAreHeadersSticky(true);
mStickyList.setDrawingListUnderStickyHeader(false);
mStickyList.setFastScrollEnabled(true);
try {
mStickyList.setFastScrollAlwaysVisible(true);
} catch (ApiLevelTooLowException e) {
}
// this view is made visible if no data is available
mStickyList.setEmptyView(getActivity().findViewById(R.id.empty));
/*
* ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only
* available for Android >= 3.0
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() {
private int count = 0;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
android.view.MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.key_list_public_multi, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Set<Integer> positions = mAdapter.getCurrentCheckedPosition();
// get IDs for checked positions as long array
long[] ids = new long[positions.size()];
int i = 0;
for (int pos : positions) {
ids[i] = mAdapter.getItemId(pos);
i++;
}
switch (item.getItemId()) {
case R.id.menu_key_list_public_multi_encrypt: {
encrypt(ids);
break;
}
case R.id.menu_key_list_public_multi_delete: {
showDeleteKeyDialog(ids);
break;
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
count = 0;
mAdapter.clearSelection();
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
if (checked) {
count++;
mAdapter.setNewSelection(position, checked);
} else {
count--;
mAdapter.removeSelection(position);
}
String keysSelected = getResources().getQuantityString(
R.plurals.key_list_selected_keys, count, count);
mode.setTitle(keysSelected);
}
});
}
// NOTE: Not supported by StickyListHeader, thus no indicator is shown while loading
// Start out with a progress indicator.
// setListShown(false);
// Create an empty adapter we will use to display the loaded data.
mAdapter = new KeyListPublicAdapter(getActivity(), null, Id.type.public_key, USER_ID_INDEX);
mStickyList.setAdapter(mAdapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
// These are the rows that we will retrieve.
static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
UserIds.USER_ID };
static final int USER_ID_INDEX = 2;
static final String SORT_ORDER = UserIds.USER_ID + " ASC";
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
Uri baseUri = KeyRings.buildPublicKeyRingsUri();
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
mStickyList.setAdapter(mAdapter);
// NOTE: Not supported by StickyListHeader, thus no indicator is shown while loading
// The list should now be shown.
// if (isResumed()) {
// setListShown(true);
// } else {
// setListShownNoAnimation(true);
// }
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
/**
* On click on item, start key view activity
*/
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
Intent viewIntent = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
} else {
viewIntent = new Intent(getActivity(), ViewKeyActivityJB.class);
}
viewIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(id)));
startActivity(viewIntent);
}
public void encrypt(long[] keyRingRowIds) {
// get master key ids from row ids
long[] keyRingIds = new long[keyRingRowIds.length];
for (int i = 0; i < keyRingRowIds.length; i++) {
keyRingIds[i] = ProviderHelper.getPublicMasterKeyId(getActivity(), keyRingRowIds[i]);
}
Intent intent = new Intent(getActivity(), EncryptActivity.class);
intent.setAction(EncryptActivity.ACTION_ENCRYPT);
intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, keyRingIds);
// used instead of startActivity set actionbar based on callingPackage
startActivityForResult(intent, 0);
}
/**
* Show dialog to delete key
*
* @param keyRingRowIds
*/
public void showDeleteKeyDialog(long[] keyRingRowIds) {
DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(null,
keyRingRowIds, Id.type.public_key);
deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog");
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ExportHelper;
import android.content.Intent;
import android.os.Bundle;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
public class KeyListSecretActivity extends DrawerActivity {
ExportHelper mExportHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mExportHelper = new ExportHelper(this);
setContentView(R.layout.key_list_secret_activity);
// now setup navigation drawer in DrawerActivity...
setupDrawerNavigation(savedInstanceState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getSupportMenuInflater().inflate(R.menu.key_list_secret, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_key_list_secret_create:
createKey();
return true;
case R.id.menu_key_list_secret_create_expert:
createKeyExpert();
return true;
case R.id.menu_key_list_secret_export:
mExportHelper.showExportKeysDialog(null, Id.type.secret_key, Constants.path.APP_DIR
+ "/secexport.asc");
return true;
case R.id.menu_key_list_secret_import:
Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE);
startActivityForResult(intentImportFromFile, 0);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void createKey() {
Intent intent = new Intent(this, EditKeyActivity.class);
intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true);
intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view
startActivityForResult(intent, 0);
}
private void createKeyExpert() {
Intent intent = new Intent(this, EditKeyActivity.class);
intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
startActivityForResult(intent, 0);
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.Set;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.ui.adapter.KeyListSecretAdapter;
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView.OnItemClickListener;
import com.actionbarsherlock.app.SherlockListFragment;
public class KeyListSecretFragment extends SherlockListFragment implements
LoaderManager.LoaderCallbacks<Cursor>, OnItemClickListener {
private KeyListSecretActivity mKeyListSecretActivity;
private KeyListSecretAdapter mAdapter;
/**
* Define Adapter and Loader on create of Activity
*/
@SuppressLint("NewApi")
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mKeyListSecretActivity = (KeyListSecretActivity) getActivity();
getListView().setOnItemClickListener(this);
// Give some text to display if there is no data. In a real
// application this would come from a resource.
setEmptyText(getString(R.string.list_empty));
/*
* ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only
* available for Android >= 3.0
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() {
private int count = 0;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
android.view.MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.key_list_secret_multi, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Set<Integer> positions = mAdapter.getCurrentCheckedPosition();
// get IDs for checked positions as long array
long[] ids = new long[positions.size()];
int i = 0;
for (int pos : positions) {
ids[i] = mAdapter.getItemId(pos);
i++;
}
switch (item.getItemId()) {
case R.id.menu_key_list_public_multi_delete: {
showDeleteKeyDialog(ids);
break;
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
count = 0;
mAdapter.clearSelection();
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
if (checked) {
count++;
mAdapter.setNewSelection(position, checked);
} else {
count--;
mAdapter.removeSelection(position);
}
String keysSelected = getResources().getQuantityString(
R.plurals.key_list_selected_keys, count, count);
mode.setTitle(keysSelected);
}
});
}
// We have a menu item to show in action bar.
setHasOptionsMenu(true);
// Start out with a progress indicator.
setListShown(false);
// Create an empty adapter we will use to display the loaded data.
mAdapter = new KeyListSecretAdapter(mKeyListSecretActivity, null, 0);
setListAdapter(mAdapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
// These are the rows that we will retrieve.
static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
UserIds.USER_ID };
static final String SORT_ORDER = UserIds.USER_ID + " COLLATE LOCALIZED ASC";
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri = KeyRings.buildSecretKeyRingsUri();
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
/**
* On click on item, start key view activity
*/
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
Intent editIntent = new Intent(mKeyListSecretActivity, EditKeyActivity.class);
editIntent.setData(KeychainContract.KeyRings.buildSecretKeyRingsUri(Long.toString(id)));
editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY);
startActivityForResult(editIntent, 0);
}
/**
* Show dialog to delete key
*
* @param keyRingRowIds
*/
public void showDeleteKeyDialog(long[] keyRingRowIds) {
DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(null,
keyRingRowIds, Id.type.secret_key);
deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog");
}
}

View File

@@ -0,0 +1,391 @@
/*
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.MenuItem;
import java.util.ArrayList;
import java.util.List;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
import org.sufficientlysecure.keychain.util.Log;
public class KeyServerQueryActivity extends SherlockFragmentActivity {
// possible intent actions for this activity
public static final String ACTION_LOOK_UP_KEY_ID = Constants.INTENT_PREFIX + "LOOK_UP_KEY_ID";
public static final String ACTION_LOOK_UP_KEY_ID_AND_RETURN = Constants.INTENT_PREFIX
+ "LOOK_UP_KEY_ID_AND_RETURN";
public static final String EXTRA_KEY_ID = "key_id";
public static final String EXTRA_FINGERPRINT = "fingerprint";
public static final String RESULT_EXTRA_TEXT = "text";
private ListView mList;
private EditText mQuery;
private Button mSearch;
private Spinner mKeyServer;
private KeyInfoListAdapter mAdapter;
private int mQueryType;
private String mQueryString;
private long mQueryId;
private volatile List<KeyInfo> mSearchResult;
private volatile String mKeyData;
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// app icon in Action Bar clicked; go home
Intent intent = new Intent(this, KeyListPublicActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
break;
}
return false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.key_server_query);
mQuery = (EditText) findViewById(R.id.query);
mSearch = (Button) findViewById(R.id.btn_search);
mList = (ListView) findViewById(R.id.list);
mAdapter = new KeyInfoListAdapter(this);
mList.setAdapter(mAdapter);
mKeyServer = (Spinner) findViewById(R.id.sign_key_keyserver);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
.getKeyServers());
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mKeyServer.setAdapter(adapter);
if (adapter.getCount() > 0) {
mKeyServer.setSelection(0);
} else {
mSearch.setEnabled(false);
}
mList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapter, View view, int position, long keyId) {
get(keyId);
}
});
mSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String query = mQuery.getText().toString();
search(query);
}
});
mQuery.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
String query = mQuery.getText().toString();
search(query);
return false; // FIXME This is a hack to hide a keyboard
// after search http://tinyurl.com/pwdc3q9
}
return false;
}
});
Intent intent = getIntent();
String action = intent.getAction();
if (ACTION_LOOK_UP_KEY_ID.equals(action) || ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(action)) {
long keyId = intent.getLongExtra(EXTRA_KEY_ID, 0);
if (keyId != 0) {
String query = "0x" + PgpKeyHelper.convertKeyToHex(keyId);
mQuery.setText(query);
search(query);
}
String fingerprint = intent.getStringExtra(EXTRA_FINGERPRINT);
if (fingerprint != null) {
fingerprint = "0x" + fingerprint;
mQuery.setText(fingerprint);
search(fingerprint);
}
}
}
private void search(String query) {
mQueryType = Id.keyserver.search;
mQueryString = query;
mAdapter.setKeys(new ArrayList<KeyInfo>());
start();
}
private void get(long keyId) {
mQueryType = Id.keyserver.get;
mQueryId = keyId;
start();
}
private void start() {
Log.d(Constants.TAG, "start search with service");
// Send all information needed to service to query keys in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_QUERY_KEYRING);
// fill values for this action
Bundle data = new Bundle();
String server = (String) mKeyServer.getSelectedItem();
data.putString(KeychainIntentService.QUERY_KEY_SERVER, server);
data.putInt(KeychainIntentService.QUERY_KEY_TYPE, mQueryType);
if (mQueryType == Id.keyserver.search) {
data.putString(KeychainIntentService.QUERY_KEY_STRING, mQueryString);
} else if (mQueryType == Id.keyserver.get) {
data.putLong(KeychainIntentService.QUERY_KEY_ID, mQueryId);
}
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after querying is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
R.string.progress_querying, ProgressDialog.STYLE_SPINNER) {
@Override
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
// get returned data bundle
Bundle returnData = message.getData();
if (mQueryType == Id.keyserver.search) {
mSearchResult = returnData
.getParcelableArrayList(KeychainIntentService.RESULT_QUERY_KEY_SEARCH_RESULT);
} else if (mQueryType == Id.keyserver.get) {
mKeyData = returnData
.getString(KeychainIntentService.RESULT_QUERY_KEY_DATA);
}
// TODO: IMPROVE CODE!!! some global variables can be
// avoided!!!
if (mQueryType == Id.keyserver.search) {
if (mSearchResult != null) {
Toast.makeText(
KeyServerQueryActivity.this,
getResources().getQuantityString(R.plurals.keys_found,
mSearchResult.size(), mSearchResult.size()),
Toast.LENGTH_SHORT).show();
mAdapter.setKeys(mSearchResult);
}
} else if (mQueryType == Id.keyserver.get) {
Intent orgIntent = getIntent();
if (ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) {
if (mKeyData != null) {
Intent intent = new Intent();
intent.putExtra(RESULT_EXTRA_TEXT, mKeyData);
setResult(RESULT_OK, intent);
} else {
setResult(RESULT_CANCELED);
}
finish();
} else {
if (mKeyData != null) {
Intent intent = new Intent(KeyServerQueryActivity.this,
ImportKeysActivity.class);
intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY);
intent.putExtra(ImportKeysActivity.EXTRA_KEY_BYTES,
mKeyData.getBytes());
startActivity(intent);
}
}
}
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
}
public class KeyInfoListAdapter extends BaseAdapter {
protected LayoutInflater mInflater;
protected Activity mActivity;
protected List<KeyInfo> mKeys;
public KeyInfoListAdapter(Activity activity) {
mActivity = activity;
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mKeys = new ArrayList<KeyInfo>();
}
public void setKeys(List<KeyInfo> keys) {
mKeys = keys;
notifyDataSetChanged();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
return mKeys.size();
}
@Override
public Object getItem(int position) {
return mKeys.get(position);
}
@Override
public long getItemId(int position) {
return mKeys.get(position).keyId;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
KeyInfo keyInfo = mKeys.get(position);
View view = mInflater.inflate(R.layout.key_server_query_result_item, null);
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
mainUserId.setText(R.string.unknown_user_id);
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
mainUserIdRest.setText("");
TextView keyId = (TextView) view.findViewById(R.id.keyId);
keyId.setText(R.string.no_key);
TextView algorithm = (TextView) view.findViewById(R.id.algorithm);
algorithm.setText("");
TextView status = (TextView) view.findViewById(R.id.status);
status.setText("");
String userId = keyInfo.userIds.get(0);
if (userId != null) {
String chunks[] = userId.split(" <", 2);
userId = chunks[0];
if (chunks.length > 1) {
mainUserIdRest.setText("<" + chunks[1]);
}
mainUserId.setText(userId);
}
keyId.setText(PgpKeyHelper.convertKeyIdToHex(keyInfo.keyId));
if (mainUserIdRest.getText().length() == 0) {
mainUserIdRest.setVisibility(View.GONE);
}
algorithm.setText("" + keyInfo.size + "/" + keyInfo.algorithm);
if (keyInfo.revoked != null) {
status.setText("revoked");
} else {
status.setVisibility(View.GONE);
}
LinearLayout ll = (LinearLayout) view.findViewById(R.id.list);
if (keyInfo.userIds.size() == 1) {
ll.setVisibility(View.GONE);
} else {
boolean first = true;
boolean second = true;
for (String uid : keyInfo.userIds) {
if (first) {
first = false;
continue;
}
if (!second) {
View sep = new View(mActivity);
sep.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark);
ll.addView(sep);
}
TextView uidView = (TextView) mInflater.inflate(
R.layout.key_server_query_result_user_id, null);
uidView.setText(uid);
ll.addView(uidView);
second = false;
}
}
return view;
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2011 Senecaso
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.beardedhen.androidbootstrap.BootstrapButton;
/**
* gpg --send-key activity
*
* Sends the selected public key to a key server
*/
public class KeyServerUploadActivity extends SherlockFragmentActivity {
// Not used in sourcode, but listed in AndroidManifest!
public static final String ACTION_EXPORT_KEY_TO_SERVER = Constants.INTENT_PREFIX
+ "EXPORT_KEY_TO_SERVER";
public static final String EXTRA_KEYRING_ROW_ID = "key_row_id";
private BootstrapButton mUploadButton;
private Spinner mKeyServerSpinner;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.key_server_export);
mUploadButton = (BootstrapButton) findViewById(R.id.btn_export_to_server);
mKeyServerSpinner = (Spinner) findViewById(R.id.sign_key_keyserver);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
.getKeyServers());
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mKeyServerSpinner.setAdapter(adapter);
if (adapter.getCount() > 0) {
mKeyServerSpinner.setSelection(0);
} else {
mUploadButton.setEnabled(false);
}
mUploadButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
uploadKey();
}
});
}
private void uploadKey() {
// Send all information needed to service to upload key in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING);
// fill values for this action
Bundle data = new Bundle();
int keyRingId = getIntent().getIntExtra(EXTRA_KEYRING_ROW_ID, -1);
data.putInt(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, keyRingId);
String server = (String) mKeyServerSpinner.getSelectedItem();
data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after uploading is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
Toast.makeText(KeyServerUploadActivity.this, R.string.key_send_success,
Toast.LENGTH_SHORT).show();
finish();
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.openpgp.PGPEncryptedData;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference;
import android.content.Intent;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceScreen;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.SherlockPreferenceActivity;
public class PreferencesActivity extends SherlockPreferenceActivity {
private IntegerListPreference mPassPhraseCacheTtl = null;
private IntegerListPreference mEncryptionAlgorithm = null;
private IntegerListPreference mHashAlgorithm = null;
private IntegerListPreference mMessageCompression = null;
private IntegerListPreference mFileCompression = null;
private CheckBoxPreference mAsciiArmour = null;
private CheckBoxPreference mForceV3Signatures = null;
private PreferenceScreen mKeyServerPreference = null;
private Preferences mPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
mPreferences = Preferences.getPreferences(this);
super.onCreate(savedInstanceState);
final ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setHomeButtonEnabled(false);
addPreferencesFromResource(R.xml.preferences);
mPassPhraseCacheTtl = (IntegerListPreference) findPreference(Constants.pref.PASS_PHRASE_CACHE_TTL);
mPassPhraseCacheTtl.setValue("" + mPreferences.getPassPhraseCacheTtl());
mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
mPassPhraseCacheTtl
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mPassPhraseCacheTtl.setValue(newValue.toString());
mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
mPreferences.setPassPhraseCacheTtl(Integer.parseInt(newValue.toString()));
return false;
}
});
mEncryptionAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM);
int valueIds[] = { PGPEncryptedData.AES_128, PGPEncryptedData.AES_192,
PGPEncryptedData.AES_256, PGPEncryptedData.BLOWFISH, PGPEncryptedData.TWOFISH,
PGPEncryptedData.CAST5, PGPEncryptedData.DES, PGPEncryptedData.TRIPLE_DES,
PGPEncryptedData.IDEA, };
String entries[] = { "AES-128", "AES-192", "AES-256", "Blowfish", "Twofish", "CAST5",
"DES", "Triple DES", "IDEA", };
String values[] = new String[valueIds.length];
for (int i = 0; i < values.length; ++i) {
values[i] = "" + valueIds[i];
}
mEncryptionAlgorithm.setEntries(entries);
mEncryptionAlgorithm.setEntryValues(values);
mEncryptionAlgorithm.setValue("" + mPreferences.getDefaultEncryptionAlgorithm());
mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
mEncryptionAlgorithm
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mEncryptionAlgorithm.setValue(newValue.toString());
mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
mPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue
.toString()));
return false;
}
});
mHashAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_HASH_ALGORITHM);
valueIds = new int[] { HashAlgorithmTags.MD5, HashAlgorithmTags.RIPEMD160,
HashAlgorithmTags.SHA1, HashAlgorithmTags.SHA224, HashAlgorithmTags.SHA256,
HashAlgorithmTags.SHA384, HashAlgorithmTags.SHA512, };
entries = new String[] { "MD5", "RIPEMD-160", "SHA-1", "SHA-224", "SHA-256", "SHA-384",
"SHA-512", };
values = new String[valueIds.length];
for (int i = 0; i < values.length; ++i) {
values[i] = "" + valueIds[i];
}
mHashAlgorithm.setEntries(entries);
mHashAlgorithm.setEntryValues(values);
mHashAlgorithm.setValue("" + mPreferences.getDefaultHashAlgorithm());
mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
mHashAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mHashAlgorithm.setValue(newValue.toString());
mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
mPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString()));
return false;
}
});
mMessageCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_MESSAGE_COMPRESSION);
valueIds = new int[] { Id.choice.compression.none, Id.choice.compression.zip,
Id.choice.compression.zlib, Id.choice.compression.bzip2, };
entries = new String[] {
getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")",
"ZIP (" + getString(R.string.compression_fast) + ")",
"ZLIB (" + getString(R.string.compression_fast) + ")",
"BZIP2 (" + getString(R.string.compression_very_slow) + ")", };
values = new String[valueIds.length];
for (int i = 0; i < values.length; ++i) {
values[i] = "" + valueIds[i];
}
mMessageCompression.setEntries(entries);
mMessageCompression.setEntryValues(values);
mMessageCompression.setValue("" + mPreferences.getDefaultMessageCompression());
mMessageCompression.setSummary(mMessageCompression.getEntry());
mMessageCompression
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mMessageCompression.setValue(newValue.toString());
mMessageCompression.setSummary(mMessageCompression.getEntry());
mPreferences.setDefaultMessageCompression(Integer.parseInt(newValue
.toString()));
return false;
}
});
mFileCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_FILE_COMPRESSION);
mFileCompression.setEntries(entries);
mFileCompression.setEntryValues(values);
mFileCompression.setValue("" + mPreferences.getDefaultFileCompression());
mFileCompression.setSummary(mFileCompression.getEntry());
mFileCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mFileCompression.setValue(newValue.toString());
mFileCompression.setSummary(mFileCompression.getEntry());
mPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString()));
return false;
}
});
mAsciiArmour = (CheckBoxPreference) findPreference(Constants.pref.DEFAULT_ASCII_ARMOUR);
mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour());
mAsciiArmour.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mAsciiArmour.setChecked((Boolean) newValue);
mPreferences.setDefaultAsciiArmour((Boolean) newValue);
return false;
}
});
mForceV3Signatures = (CheckBoxPreference) findPreference(Constants.pref.FORCE_V3_SIGNATURES);
mForceV3Signatures.setChecked(mPreferences.getForceV3Signatures());
mForceV3Signatures
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mForceV3Signatures.setChecked((Boolean) newValue);
mPreferences.setForceV3Signatures((Boolean) newValue);
return false;
}
});
mKeyServerPreference = (PreferenceScreen) findPreference(Constants.pref.KEY_SERVERS);
String servers[] = mPreferences.getKeyServers();
mKeyServerPreference.setSummary(getResources().getQuantityString(R.plurals.n_key_servers,
servers.length, servers.length));
mKeyServerPreference
.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(PreferencesActivity.this,
PreferencesKeyServerActivity.class);
intent.putExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS,
mPreferences.getKeyServers());
startActivityForResult(intent, Id.request.key_server_preference);
return false;
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case Id.request.key_server_preference: {
if (resultCode == RESULT_CANCELED || data == null) {
return;
}
String servers[] = data
.getStringArrayExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS);
mPreferences.setKeyServers(servers);
mKeyServerPreference.setSummary(getResources().getQuantityString(
R.plurals.n_key_servers, servers.length, servers.length));
break;
}
default: {
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.Vector;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import org.sufficientlysecure.keychain.ui.widget.Editor;
import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import com.actionbarsherlock.app.SherlockActivity;
public class PreferencesKeyServerActivity extends SherlockActivity implements OnClickListener,
EditorListener {
public static final String EXTRA_KEY_SERVERS = "key_servers";
private LayoutInflater mInflater;
private ViewGroup mEditors;
private View mAdd;
private TextView mTitle;
private TextView mSummary;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Inflate a "Done"/"Cancel" custom action bar view
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// ok
okClicked();
}
}, R.string.btn_do_not_save, new View.OnClickListener() {
@Override
public void onClick(View v) {
// cancel
cancelClicked();
}
});
setContentView(R.layout.key_server_preference);
mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mTitle = (TextView) findViewById(R.id.title);
mSummary = (TextView) findViewById(R.id.summary);
mTitle.setText(R.string.label_key_servers);
mEditors = (ViewGroup) findViewById(R.id.editors);
mAdd = findViewById(R.id.add);
mAdd.setOnClickListener(this);
Intent intent = getIntent();
String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS);
if (servers != null) {
for (int i = 0; i < servers.length; ++i) {
KeyServerEditor view = (KeyServerEditor) mInflater.inflate(
R.layout.key_server_editor, mEditors, false);
view.setEditorListener(this);
view.setValue(servers[i]);
mEditors.addView(view);
}
}
}
public void onDeleted(Editor editor) {
// nothing to do
}
public void onClick(View v) {
KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor,
mEditors, false);
view.setEditorListener(this);
mEditors.addView(view);
}
private void cancelClicked() {
setResult(RESULT_CANCELED, null);
finish();
}
private void okClicked() {
Intent data = new Intent();
Vector<String> servers = new Vector<String>();
for (int i = 0; i < mEditors.getChildCount(); ++i) {
KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i);
String tmp = editor.getValue();
if (tmp.length() > 0) {
servers.add(tmp);
}
}
String[] dummy = new String[0];
data.putExtra(EXTRA_KEY_SERVERS, servers.toArray(dummy));
setResult(RESULT_OK, data);
finish();
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import com.actionbarsherlock.app.SherlockFragmentActivity;
public class SelectPublicKeyActivity extends SherlockFragmentActivity {
// Actions for internal use only:
public static final String ACTION_SELECT_PUBLIC_KEYS = Constants.INTENT_PREFIX
+ "SELECT_PUBLIC_KEYRINGS";
public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids";
public static final String RESULT_EXTRA_MASTER_KEY_IDS = "master_key_ids";
public static final String RESULT_EXTRA_USER_IDS = "user_ids";
SelectPublicKeyFragment mSelectFragment;
long selectedMasterKeyIds[];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Inflate a "Done"/"Cancel" custom action bar view
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// ok
okClicked();
}
}, R.string.btn_do_not_save, new View.OnClickListener() {
@Override
public void onClick(View v) {
// cancel
cancelClicked();
}
});
setContentView(R.layout.select_public_key_activity);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
handleIntent(getIntent());
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.select_public_key_fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of the fragment
mSelectFragment = SelectPublicKeyFragment.newInstance(selectedMasterKeyIds);
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.select_public_key_fragment_container, mSelectFragment).commit();
}
// TODO: reimplement!
// mFilterLayout = findViewById(R.id.layout_filter);
// mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
// mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
//
// mClearFilterButton.setOnClickListener(new OnClickListener() {
// public void onClick(View v) {
// handleIntent(new Intent());
// }
// });
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
// TODO: reimplement search!
// String searchString = null;
// if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
// searchString = intent.getStringExtra(SearchManager.QUERY);
// if (searchString != null && searchString.trim().length() == 0) {
// searchString = null;
// }
// }
// if (searchString == null) {
// mFilterLayout.setVisibility(View.GONE);
// } else {
// mFilterLayout.setVisibility(View.VISIBLE);
// mFilterInfo.setText(getString(R.string.filterInfo, searchString));
// }
// preselected master keys
selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
}
private void cancelClicked() {
setResult(RESULT_CANCELED, null);
finish();
}
private void okClicked() {
Intent data = new Intent();
data.putExtra(RESULT_EXTRA_MASTER_KEY_IDS, mSelectFragment.getSelectedMasterKeyIds());
data.putExtra(RESULT_EXTRA_USER_IDS, mSelectFragment.getSelectedUserIds());
setResult(RESULT_OK, data);
finish();
}
}

View File

@@ -0,0 +1,253 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.Date;
import java.util.Vector;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
import android.app.Activity;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.widget.ListView;
public class SelectPublicKeyFragment extends ListFragmentWorkaround implements
LoaderManager.LoaderCallbacks<Cursor> {
public static final String ARG_PRESELECTED_KEY_IDS = "preselected_key_ids";
private Activity mActivity;
private SelectKeyCursorAdapter mAdapter;
private ListView mListView;
private long mSelectedMasterKeyIds[];
/**
* Creates new instance of this fragment
*/
public static SelectPublicKeyFragment newInstance(long[] preselectedKeyIds) {
SelectPublicKeyFragment frag = new SelectPublicKeyFragment();
Bundle args = new Bundle();
args.putLongArray(ARG_PRESELECTED_KEY_IDS, preselectedKeyIds);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSelectedMasterKeyIds = getArguments().getLongArray(ARG_PRESELECTED_KEY_IDS);
}
/**
* Define Adapter and Loader on create of Activity
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mActivity = getSherlockActivity();
mListView = getListView();
mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
// Give some text to display if there is no data. In a real
// application this would come from a resource.
setEmptyText(getString(R.string.list_empty));
mAdapter = new SelectKeyCursorAdapter(mActivity, null, 0, mListView, Id.type.public_key);
setListAdapter(mAdapter);
// Start out with a progress indicator.
setListShown(false);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
/**
* Selects items based on master key ids in list view
*
* @param masterKeyIds
*/
private void preselectMasterKeyIds(long[] masterKeyIds) {
if (masterKeyIds != null) {
for (int i = 0; i < mListView.getCount(); ++i) {
long keyId = mAdapter.getMasterKeyId(i);
for (int j = 0; j < masterKeyIds.length; ++j) {
if (keyId == masterKeyIds[j]) {
mListView.setItemChecked(i, true);
break;
}
}
}
}
}
/**
* Returns all selected master key ids
*
* @return
*/
public long[] getSelectedMasterKeyIds() {
// mListView.getCheckedItemIds() would give the row ids of the KeyRings not the master key
// ids!
Vector<Long> vector = new Vector<Long>();
for (int i = 0; i < mListView.getCount(); ++i) {
if (mListView.isItemChecked(i)) {
vector.add(mAdapter.getMasterKeyId(i));
}
}
// convert to long array
long[] selectedMasterKeyIds = new long[vector.size()];
for (int i = 0; i < vector.size(); ++i) {
selectedMasterKeyIds[i] = vector.get(i);
}
return selectedMasterKeyIds;
}
/**
* Returns all selected user ids
*
* @return
*/
public String[] getSelectedUserIds() {
Vector<String> userIds = new Vector<String>();
for (int i = 0; i < mListView.getCount(); ++i) {
if (mListView.isItemChecked(i)) {
userIds.add((String) mAdapter.getUserId(i));
}
}
// make empty array to not return null
String userIdArray[] = new String[0];
return userIds.toArray(userIdArray);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
Uri baseUri = KeyRings.buildPublicKeyRingsUri();
// These are the rows that we will retrieve.
long now = new Date().getTime() / 1000;
String[] projection = new String[] {
KeyRings._ID,
KeyRings.MASTER_KEY_ID,
UserIds.USER_ID,
"(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
+ Keys.CAN_ENCRYPT + " = '1') AS "
+ SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
"(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ " AND valid_keys." + Keys.IS_REVOKED + " = '0' AND valid_keys."
+ Keys.CAN_ENCRYPT + " = '1' AND valid_keys." + Keys.CREATION + " <= '"
+ now + "' AND " + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys."
+ Keys.EXPIRY + " >= '" + now + "')) AS "
+ SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
String inMasterKeyList = null;
if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) {
inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN (";
for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) {
if (i != 0) {
inMasterKeyList += ", ";
}
inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]);
}
inMasterKeyList += ")";
}
// if (searchString != null && searchString.trim().length() > 0) {
// String[] chunks = searchString.trim().split(" +");
// qb.appendWhere("(EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
// + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "."
// + Keys._ID);
// for (int i = 0; i < chunks.length; ++i) {
// qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
// qb.appendWhereEscapeString("%" + chunks[i] + "%");
// }
// qb.appendWhere("))");
//
// if (inIdList != null) {
// qb.appendWhere(" OR (" + inIdList + ")");
// }
// }
String orderBy = UserIds.USER_ID + " ASC";
if (inMasterKeyList != null) {
// sort by selected master keys
orderBy = inMasterKeyList + " DESC, " + orderBy;
}
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(getActivity(), baseUri, projection, null, null, orderBy);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
// preselect given master keys
preselectMasterKeyIds(mSelectedMasterKeyIds);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import android.content.Intent;
import android.os.Bundle;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
public class SelectSecretKeyActivity extends SherlockFragmentActivity {
// Actions for internal use only:
public static final String ACTION_SELECT_SECRET_KEY = Constants.INTENT_PREFIX
+ "SELECT_SECRET_KEYRING";
public static final String EXTRA_FILTER_CERTIFY = "filter_certify";
public static final String RESULT_EXTRA_MASTER_KEY_ID = "master_key_id";
public static final String RESULT_EXTRA_USER_ID = "user_id";
private boolean mFilterCertify = false;
private SelectSecretKeyFragment mSelectFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.select_secret_key_activity);
final ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setHomeButtonEnabled(false);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// TODO: reimplement!
// mFilterLayout = findViewById(R.id.layout_filter);
// mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
// mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
//
// mClearFilterButton.setOnClickListener(new OnClickListener() {
// public void onClick(View v) {
// handleIntent(new Intent());
// }
// });
mFilterCertify = getIntent().getBooleanExtra(EXTRA_FILTER_CERTIFY, false);
handleIntent(getIntent());
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.select_secret_key_fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of the fragment
mSelectFragment = SelectSecretKeyFragment.newInstance(mFilterCertify);
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.select_secret_key_fragment_container, mSelectFragment).commit();
}
}
/**
* This is executed by SelectSecretKeyFragment after clicking on an item
*
* @param masterKeyId
* @param userId
*/
public void afterListSelection(long masterKeyId, String userId) {
Intent data = new Intent();
data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, masterKeyId);
data.putExtra(RESULT_EXTRA_USER_ID, (String) userId);
setResult(RESULT_OK, data);
finish();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
// TODO: reimplement!
// String searchString = null;
// if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
// searchString = intent.getStringExtra(SearchManager.QUERY);
// if (searchString != null && searchString.trim().length() == 0) {
// searchString = null;
// }
// }
// if (searchString == null) {
// mFilterLayout.setVisibility(View.GONE);
// } else {
// mFilterLayout.setVisibility(View.VISIBLE);
// mFilterInfo.setText(getString(R.string.filterInfo, searchString));
// }
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// TODO: reimplement!
// menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon(
// android.R.drawable.ic_menu_search);
return true;
}
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.Date;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import com.actionbarsherlock.app.SherlockListFragment;
public class SelectSecretKeyFragment extends SherlockListFragment implements
LoaderManager.LoaderCallbacks<Cursor> {
private SelectSecretKeyActivity mActivity;
private SelectKeyCursorAdapter mAdapter;
private ListView mListView;
private boolean mFilterCertify;
private static final String ARG_FILTER_CERTIFY = "filter_certify";
/**
* Creates new instance of this fragment
*/
public static SelectSecretKeyFragment newInstance(boolean filterCertify) {
SelectSecretKeyFragment frag = new SelectSecretKeyFragment();
Bundle args = new Bundle();
args.putBoolean(ARG_FILTER_CERTIFY, filterCertify);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFilterCertify = getArguments().getBoolean(ARG_FILTER_CERTIFY);
}
/**
* Define Adapter and Loader on create of Activity
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mActivity = (SelectSecretKeyActivity) getSherlockActivity();
mListView = getListView();
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
long masterKeyId = mAdapter.getMasterKeyId(position);
String userId = mAdapter.getUserId(position);
// return data to activity, which results in finishing it
mActivity.afterListSelection(masterKeyId, userId);
}
});
// Give some text to display if there is no data. In a real
// application this would come from a resource.
setEmptyText(getString(R.string.list_empty));
mAdapter = new SelectKeyCursorAdapter(mActivity, null, 0, mListView, Id.type.secret_key);
setListAdapter(mAdapter);
// Start out with a progress indicator.
setListShown(false);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
Uri baseUri = KeyRings.buildSecretKeyRingsUri();
String CapFilter = null;
if (mFilterCertify) {
CapFilter = "(cert > 0)";
}
// These are the rows that we will retrieve.
long now = new Date().getTime() / 1000;
String[] projection = new String[] {
KeyRings._ID,
KeyRings.MASTER_KEY_ID,
UserIds.USER_ID,
"(SELECT COUNT(cert_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ " AS cert_keys WHERE cert_keys." + Keys.KEY_RING_ROW_ID + " = "
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND cert_keys."
+ Keys.CAN_CERTIFY + " = '1') AS cert",
"(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
+ Keys.CAN_SIGN + " = '1') AS "
+ SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
"(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys."
+ Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_SIGN
+ " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND "
+ "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY
+ " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
// if (searchString != null && searchString.trim().length() > 0) {
// String[] chunks = searchString.trim().split(" +");
// qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
// + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "."
// + Keys._ID);
// for (int i = 0; i < chunks.length; ++i) {
// qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
// qb.appendWhereEscapeString("%" + chunks[i] + "%");
// }
// qb.appendWhere(")");
// }
String orderBy = UserIds.USER_ID + " ASC";
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(getActivity(), baseUri, projection, CapFilter, null, orderBy);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class SelectSecretKeyLayoutFragment extends Fragment {
private TextView mKeyUserId;
private TextView mKeyUserIdRest;
private BootstrapButton mSelectKeyButton;
private SelectSecretKeyCallback mCallback;
private static final int REQUEST_CODE_SELECT_KEY = 8882;
public interface SelectSecretKeyCallback {
void onKeySelected(long secretKeyId);
}
public void setCallback(SelectSecretKeyCallback callback) {
mCallback = callback;
}
public void selectKey(long secretKeyId) {
if (secretKeyId == Id.key.none) {
mKeyUserId.setText(R.string.api_settings_no_key);
mKeyUserIdRest.setText("");
} else {
String uid = getResources().getString(R.string.unknown_user_id);
String uidExtra = "";
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
getActivity(), secretKeyId);
if (keyRing != null) {
PGPSecretKey key = PgpKeyHelper.getMasterKey(keyRing);
if (key != null) {
String userId = PgpKeyHelper.getMainUserIdSafe(getActivity(), key);
String chunks[] = userId.split(" <", 2);
uid = chunks[0];
if (chunks.length > 1) {
uidExtra = "<" + chunks[1];
}
}
}
mKeyUserId.setText(uid);
mKeyUserIdRest.setText(uidExtra);
}
}
public void setError(String error) {
mKeyUserId.requestFocus();
mKeyUserId.setError(error);
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.select_secret_key_layout_fragment, container, false);
mKeyUserId = (TextView) view.findViewById(R.id.select_secret_key_user_id);
mKeyUserIdRest = (TextView) view.findViewById(R.id.select_secret_key_user_id_rest);
mSelectKeyButton = (BootstrapButton) view
.findViewById(R.id.select_secret_key_select_key_button);
mSelectKeyButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startSelectKeyActivity();
}
});
return view;
}
private void startSelectKeyActivity() {
Intent intent = new Intent(getActivity(), SelectSecretKeyActivity.class);
startActivityForResult(intent, REQUEST_CODE_SELECT_KEY);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode & 0xFFFF) {
case REQUEST_CODE_SELECT_KEY: {
long secretKeyId;
if (resultCode == Activity.RESULT_OK) {
Bundle bundle = data.getExtras();
secretKeyId = bundle.getLong(SelectSecretKeyActivity.RESULT_EXTRA_MASTER_KEY_ID);
selectKey(secretKeyId);
// remove displayed errors
mKeyUserId.setError(null);
// give value back to callback
mCallback.onKeySelected(secretKeyId);
}
break;
}
default:
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
}

View File

@@ -0,0 +1,308 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2011 Senecaso
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.Iterator;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSignature;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Spinner;
import android.widget.Toast;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.beardedhen.androidbootstrap.BootstrapButton;
/**
* gpg --sign-key
*
* signs the specified public key with the specified secret master key
*/
public class SignKeyActivity extends SherlockFragmentActivity implements
SelectSecretKeyLayoutFragment.SelectSecretKeyCallback {
public static final String EXTRA_KEY_ID = "key_id";
private long mPubKeyId = 0;
private long mMasterKeyId = 0;
private BootstrapButton mSignButton;
private CheckBox mUploadKeyCheckbox;
private Spinner mSelectKeyserverSpinner;
private SelectSecretKeyLayoutFragment mSelectKeyFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sign_key_activity);
final ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setHomeButtonEnabled(false);
mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getSupportFragmentManager()
.findFragmentById(R.id.sign_key_select_key_fragment);
mSelectKeyFragment.setCallback(this);
mSelectKeyserverSpinner = (Spinner) findViewById(R.id.sign_key_keyserver);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
.getKeyServers());
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSelectKeyserverSpinner.setAdapter(adapter);
mUploadKeyCheckbox = (CheckBox) findViewById(R.id.sign_key_upload_checkbox);
if (!mUploadKeyCheckbox.isChecked()) {
mSelectKeyserverSpinner.setEnabled(false);
} else {
mSelectKeyserverSpinner.setEnabled(true);
}
mUploadKeyCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (!isChecked) {
mSelectKeyserverSpinner.setEnabled(false);
} else {
mSelectKeyserverSpinner.setEnabled(true);
}
}
});
mSignButton = (BootstrapButton) findViewById(R.id.sign_key_sign_button);
mSignButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mPubKeyId != 0) {
if (mMasterKeyId == 0) {
mSelectKeyFragment.setError(getString(R.string.select_key_to_sign));
} else {
initiateSigning();
}
}
}
});
mPubKeyId = getIntent().getLongExtra(EXTRA_KEY_ID, 0);
if (mPubKeyId == 0) {
Log.e(Constants.TAG, "No pub key id given!");
finish();
}
}
private void showPassphraseDialog(final long secretKeyId) {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
startSigning();
}
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
try {
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
messenger, secretKeyId);
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
} catch (PgpGeneralException e) {
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
// send message to handler to start encryption directly
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
}
}
/**
* handles the UI bits of the signing process on the UI thread
*/
private void initiateSigning() {
PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this, mPubKeyId);
if (pubring != null) {
// if we have already signed this key, dont bother doing it again
boolean alreadySigned = false;
@SuppressWarnings("unchecked")
Iterator<PGPSignature> itr = pubring.getPublicKey(mPubKeyId).getSignatures();
while (itr.hasNext()) {
PGPSignature sig = itr.next();
if (sig.getKeyID() == mMasterKeyId) {
alreadySigned = true;
break;
}
}
if (!alreadySigned) {
/*
* get the user's passphrase for this key (if required)
*/
String passphrase = PassphraseCacheService.getCachedPassphrase(this, mMasterKeyId);
if (passphrase == null) {
showPassphraseDialog(mMasterKeyId);
return; // bail out; need to wait until the user has entered the passphrase
// before trying again
} else {
startSigning();
}
} else {
Toast.makeText(this, R.string.key_has_already_been_signed, Toast.LENGTH_SHORT)
.show();
setResult(RESULT_CANCELED);
finish();
}
}
}
/**
* kicks off the actual signing process on a background thread
*/
private void startSigning() {
// Send all information needed to service to sign key in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_SIGN_KEYRING);
// fill values for this action
Bundle data = new Bundle();
data.putLong(KeychainIntentService.SIGN_KEY_MASTER_KEY_ID, mMasterKeyId);
data.putLong(KeychainIntentService.SIGN_KEY_PUB_KEY_ID, mPubKeyId);
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after signing is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
R.string.progress_signing, ProgressDialog.STYLE_SPINNER) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
Toast.makeText(SignKeyActivity.this, R.string.key_sign_success,
Toast.LENGTH_SHORT).show();
// check if we need to send the key to the server or not
if (mUploadKeyCheckbox.isChecked()) {
/*
* upload the newly signed key to the key server
*/
uploadKey();
} else {
finish();
}
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
}
private void uploadKey() {
// Send all information needed to service to upload key in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING);
// fill values for this action
Bundle data = new Bundle();
data.putLong(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, mPubKeyId);
Spinner keyServer = (Spinner) findViewById(R.id.sign_key_keyserver);
String server = (String) keyServer.getSelectedItem();
data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Message is received after uploading is done in ApgService
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
public void handleMessage(Message message) {
// handle messages by standard ApgHandler first
super.handleMessage(message);
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
Toast.makeText(SignKeyActivity.this, R.string.key_send_success,
Toast.LENGTH_SHORT).show();
finish();
}
};
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
}
/**
* callback from select key fragment
*/
@Override
public void onKeySelected(long secretKeyId) {
mMasterKeyId = secretKeyId;
}
}

View File

@@ -0,0 +1,484 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2013 Bahtiar 'kalkin' Gadimov
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import java.util.ArrayList;
import java.util.Date;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import org.sufficientlysecure.keychain.helper.ExportHelper;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.ViewKeyKeysAdapter;
import org.sufficientlysecure.keychain.ui.adapter.ViewKeyUserIdsAdapter;
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment;
import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.format.DateFormat;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class ViewKeyActivity extends SherlockFragmentActivity implements
LoaderManager.LoaderCallbacks<Cursor> {
ExportHelper mExportHelper;
protected Uri mDataUri;
private TextView mName;
private TextView mEmail;
private TextView mComment;
private TextView mAlgorithm;
private TextView mKeyId;
private TextView mExpiry;
private TextView mCreation;
private TextView mFingerprint;
private BootstrapButton mActionEncrypt;
private ListView mUserIds;
private ListView mKeys;
private static final int LOADER_ID_KEYRING = 0;
private static final int LOADER_ID_USER_IDS = 1;
private static final int LOADER_ID_KEYS = 2;
private ViewKeyUserIdsAdapter mUserIdsAdapter;
private ViewKeyKeysAdapter mKeysAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mExportHelper = new ExportHelper(this);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setIcon(android.R.color.transparent);
getSupportActionBar().setHomeButtonEnabled(true);
setContentView(R.layout.view_key_activity);
mName = (TextView) findViewById(R.id.name);
mEmail = (TextView) findViewById(R.id.email);
mComment = (TextView) findViewById(R.id.comment);
mKeyId = (TextView) findViewById(R.id.key_id);
mAlgorithm = (TextView) findViewById(R.id.algorithm);
mCreation = (TextView) findViewById(R.id.creation);
mExpiry = (TextView) findViewById(R.id.expiry);
mFingerprint = (TextView) findViewById(R.id.fingerprint);
mActionEncrypt = (BootstrapButton) findViewById(R.id.action_encrypt);
mUserIds = (ListView) findViewById(R.id.user_ids);
mKeys = (ListView) findViewById(R.id.keys);
Intent intent = getIntent();
mDataUri = intent.getData();
if (mDataUri == null) {
Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
finish();
return;
} else {
Log.d(Constants.TAG, "uri: " + mDataUri);
loadData(mDataUri);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getSupportMenuInflater().inflate(R.menu.key_view, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent homeIntent = new Intent(this, KeyListPublicActivity.class);
homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(homeIntent);
return true;
case R.id.menu_key_view_update:
updateFromKeyserver(mDataUri);
return true;
case R.id.menu_key_view_sign:
signKey(mDataUri);
return true;
case R.id.menu_key_view_export_keyserver:
uploadToKeyserver(mDataUri);
return true;
case R.id.menu_key_view_export_file:
mExportHelper.showExportKeysDialog(mDataUri, Id.type.public_key, Constants.path.APP_DIR
+ "/pubexport.asc");
return true;
case R.id.menu_key_view_share_default_fingerprint:
shareKey(mDataUri, true);
return true;
case R.id.menu_key_view_share_default:
shareKey(mDataUri, false);
return true;
case R.id.menu_key_view_share_qr_code_fingerprint:
shareKeyQrCode(mDataUri, true);
return true;
case R.id.menu_key_view_share_qr_code:
shareKeyQrCode(mDataUri, false);
return true;
case R.id.menu_key_view_share_nfc:
shareNfc();
return true;
case R.id.menu_key_view_share_clipboard:
copyToClipboard(mDataUri);
return true;
case R.id.menu_key_view_delete: {
// Message is received after key is deleted
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) {
setResult(RESULT_CANCELED);
finish();
}
}
};
mExportHelper.deleteKey(mDataUri, Id.type.public_key, returnHandler);
return true;
}
}
return super.onOptionsItemSelected(item);
}
private void loadData(Uri dataUri) {
mActionEncrypt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO: don't get object here!!! solve this differently!
PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(
ViewKeyActivity.this, mDataUri);
PGPPublicKey publicKey = ring.getPublicKey();
long[] encryptionKeyIds = new long[] { publicKey.getKeyID() };
Intent intent = new Intent(ViewKeyActivity.this, EncryptActivity.class);
intent.setAction(EncryptActivity.ACTION_ENCRYPT);
intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds);
// used instead of startActivity set actionbar based on callingPackage
startActivityForResult(intent, 0);
}
});
mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0);
mUserIds.setAdapter(mUserIdsAdapter);
// mUserIds.setEmptyView(findViewById(android.R.id.empty));
// mUserIds.setClickable(true);
// mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() {
// @Override
// public void onItemClick(AdapterView<?> arg0, View arg1, int position, long id) {
// }
// });
mKeysAdapter = new ViewKeyKeysAdapter(this, null, 0);
mKeys.setAdapter(mKeysAdapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getSupportLoaderManager().initLoader(LOADER_ID_KEYRING, null, this);
getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
getSupportLoaderManager().initLoader(LOADER_ID_KEYS, null, this);
}
static final String[] KEYRING_PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
UserIds.USER_ID };
static final int KEYRING_INDEX_ID = 0;
static final int KEYRING_INDEX_MASTER_KEY_ID = 1;
static final int KEYRING_INDEX_USER_ID = 2;
static final String[] USER_IDS_PROJECTION = new String[] { UserIds._ID, UserIds.USER_ID,
UserIds.RANK, };
// not the main user id
static final String USER_IDS_SELECTION = UserIds.RANK + " > 0 ";
static final String USER_IDS_SORT_ORDER = UserIds.USER_ID + " COLLATE LOCALIZED ASC";
static final String[] KEYS_PROJECTION = new String[] { Keys._ID, Keys.KEY_ID,
Keys.IS_MASTER_KEY, Keys.ALGORITHM, Keys.KEY_SIZE, Keys.CAN_CERTIFY, Keys.CAN_SIGN,
Keys.CAN_ENCRYPT, Keys.CREATION, Keys.EXPIRY };
static final String KEYS_SORT_ORDER = Keys.RANK + " ASC";
static final int KEYS_INDEX_ID = 0;
static final int KEYS_INDEX_KEY_ID = 1;
static final int KEYS_INDEX_IS_MASTER_KEY = 2;
static final int KEYS_INDEX_ALGORITHM = 3;
static final int KEYS_INDEX_KEY_SIZE = 4;
static final int KEYS_INDEX_CAN_CERTIFY = 5;
static final int KEYS_INDEX_CAN_SIGN = 6;
static final int KEYS_INDEX_CAN_ENCRYPT = 7;
static final int KEYS_INDEX_CREATION = 8;
static final int KEYS_INDEX_EXPIRY = 9;
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_ID_KEYRING: {
Uri baseUri = mDataUri;
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(this, baseUri, KEYRING_PROJECTION, null, null, null);
}
case LOADER_ID_USER_IDS: {
Uri baseUri = UserIds.buildUserIdsUri(mDataUri);
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(this, baseUri, USER_IDS_PROJECTION, USER_IDS_SELECTION, null,
USER_IDS_SORT_ORDER);
}
case LOADER_ID_KEYS: {
Uri baseUri = Keys.buildKeysUri(mDataUri);
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(this, baseUri, KEYS_PROJECTION, null, null, KEYS_SORT_ORDER);
}
default:
return null;
}
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
switch (loader.getId()) {
case LOADER_ID_KEYRING:
if (data.moveToFirst()) {
// get name, email, and comment from USER_ID
String[] mainUserId = PgpKeyHelper.splitUserId(data
.getString(KEYRING_INDEX_USER_ID));
setTitle(mainUserId[0]);
mName.setText(mainUserId[0]);
mEmail.setText(mainUserId[1]);
mComment.setText(mainUserId[2]);
}
break;
case LOADER_ID_USER_IDS:
mUserIdsAdapter.swapCursor(data);
break;
case LOADER_ID_KEYS:
// the first key here is our master key
if (data.moveToFirst()) {
// get key id from MASTER_KEY_ID
long keyId = data.getLong(KEYS_INDEX_KEY_ID);
String keyIdStr = "0x" + PgpKeyHelper.convertKeyIdToHex(keyId);
mKeyId.setText(keyIdStr);
// get creation date from CREATION
if (data.isNull(KEYS_INDEX_CREATION)) {
mCreation.setText(R.string.none);
} else {
Date creationDate = new Date(data.getLong(KEYS_INDEX_CREATION) * 1000);
mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format(
creationDate));
}
// get creation date from EXPIRY
if (data.isNull(KEYS_INDEX_EXPIRY)) {
mExpiry.setText(R.string.none);
} else {
Date expiryDate = new Date(data.getLong(KEYS_INDEX_EXPIRY) * 1000);
mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(
expiryDate));
}
String algorithmStr = PgpKeyHelper.getAlgorithmInfo(
data.getInt(KEYS_INDEX_ALGORITHM), data.getInt(KEYS_INDEX_KEY_SIZE));
mAlgorithm.setText(algorithmStr);
// TODO: Can this be done better? fingerprint in db?
String fingerprint = PgpKeyHelper.getFingerPrint(this, keyId);
fingerprint = fingerprint.replace(" ", "\n");
mFingerprint.setText(fingerprint);
// TODO: get image with getUserAttributes() on key and then
// PGPUserAttributeSubpacketVector
}
mKeysAdapter.swapCursor(data);
break;
default:
break;
}
}
/**
* This is called when the last Cursor provided to onLoadFinished() above is about to be closed.
* We need to make sure we are no longer using it.
*/
public void onLoaderReset(Loader<Cursor> loader) {
switch (loader.getId()) {
case LOADER_ID_KEYRING:
// TODO?
break;
case LOADER_ID_USER_IDS:
mUserIdsAdapter.swapCursor(null);
break;
case LOADER_ID_KEYS:
mKeysAdapter.swapCursor(null);
break;
default:
break;
}
}
private void uploadToKeyserver(Uri dataUri) {
long keyRingRowId = Long.valueOf(dataUri.getLastPathSegment());
Intent uploadIntent = new Intent(this, KeyServerUploadActivity.class);
uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER);
uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int) keyRingRowId);
startActivityForResult(uploadIntent, Id.request.export_to_server);
}
private void updateFromKeyserver(Uri dataUri) {
long updateKeyId = 0;
PGPPublicKeyRing updateRing = (PGPPublicKeyRing) ProviderHelper
.getPGPKeyRing(this, dataUri);
if (updateRing != null) {
updateKeyId = PgpKeyHelper.getMasterKey(updateRing).getKeyID();
}
if (updateKeyId == 0) {
Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!");
return;
}
Intent queryIntent = new Intent(this, KeyServerQueryActivity.class);
queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN);
queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId);
// TODO: lookup??
startActivityForResult(queryIntent, Id.request.look_up_key_id);
}
private void signKey(Uri dataUri) {
long keyId = 0;
PGPPublicKeyRing signKey = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri);
if (signKey != null) {
keyId = PgpKeyHelper.getMasterKey(signKey).getKeyID();
}
if (keyId == 0) {
Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!");
return;
}
Intent signIntent = new Intent(this, SignKeyActivity.class);
signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId);
startActivity(signIntent);
}
private void shareKey(Uri dataUri, boolean fingerprintOnly) {
String content = null;
if (fingerprintOnly) {
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
// TODO: dublicated in ShareQrCodeDialog
content = "openpgp4fpr:";
String fingerprint = PgpKeyHelper.convertKeyToHex(masterKeyId);
content = content + fingerprint;
} else {
// get public keyring as ascii armored string
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this,
dataUri, new long[] { masterKeyId });
content = keyringArmored.get(0);
// Android will fail with android.os.TransactionTooLargeException if key is too big
// see http://www.lonestarprod.com/?p=34
if (content.length() >= 86389) {
Toast.makeText(getApplicationContext(), R.string.key_too_big_for_sharing,
Toast.LENGTH_LONG).show();
return;
}
}
// let user choose application
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, content);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent,
getResources().getText(R.string.action_share_key_with)));
}
private void shareKeyQrCode(Uri dataUri, boolean fingerprintOnly) {
ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(dataUri,
fingerprintOnly);
dialog.show(getSupportFragmentManager(), "shareQrCodeDialog");
}
private void copyToClipboard(Uri dataUri) {
// get public keyring as ascii armored string
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri,
new long[] { masterKeyId });
ClipboardReflection.copyToClipboard(this, keyringArmored.get(0));
Toast.makeText(getApplicationContext(), R.string.key_copied_to_clipboard, Toast.LENGTH_LONG)
.show();
}
private void shareNfc() {
ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance();
dialog.show(getSupportFragmentManager(), "shareNfcDialog");
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2013 Bahtiar 'kalkin' Gadimov
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import android.annotation.TargetApi;
import android.database.Cursor;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.NfcAdapter.CreateNdefMessageCallback;
import android.nfc.NfcAdapter.OnNdefPushCompleteCallback;
import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.LoaderManager;
import android.widget.Toast;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class ViewKeyActivityJB extends ViewKeyActivity implements CreateNdefMessageCallback,
OnNdefPushCompleteCallback, LoaderManager.LoaderCallbacks<Cursor> {
// NFC
private NfcAdapter mNfcAdapter;
private byte[] mSharedKeyringBytes;
private static final int NFC_SENT = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initNfc(mDataUri);
}
/**
* NFC: Initialize NFC sharing if OS and device supports it
*/
private void initNfc(Uri dataUri) {
// check if NFC Beam is supported (>= Android 4.1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// Check for available NFC Adapter
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter != null) {
// init nfc
// get public keyring as byte array
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
mSharedKeyringBytes = ProviderHelper.getKeyRingsAsByteArray(this, dataUri,
new long[] { masterKeyId });
// Register callback to set NDEF message
mNfcAdapter.setNdefPushMessageCallback(this, this);
// Register callback to listen for message-sent success
mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
}
}
}
/**
* NFC: Implementation for the CreateNdefMessageCallback interface
*/
@Override
public NdefMessage createNdefMessage(NfcEvent event) {
/**
* When a device receives a push with an AAR in it, the application specified in the AAR is
* guaranteed to run. The AAR overrides the tag dispatch system. You can add it back in to
* guarantee that this activity starts when receiving a beamed message. For now, this code
* uses the tag dispatch system.
*/
NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME,
mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME));
return msg;
}
/**
* NFC: Implementation for the OnNdefPushCompleteCallback interface
*/
@Override
public void onNdefPushComplete(NfcEvent arg0) {
// A handler is needed to send messages to the activity when this
// callback occurs, because it happens from a binder thread
mNfcHandler.obtainMessage(NFC_SENT).sendToTarget();
}
/**
* NFC: This handler receives a message from onNdefPushComplete
*/
private final Handler mNfcHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case NFC_SENT:
Toast.makeText(getApplicationContext(), R.string.nfc_successfull, Toast.LENGTH_LONG)
.show();
break;
}
}
};
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import java.util.List;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> {
protected LayoutInflater mInflater;
protected Activity mActivity;
protected List<ImportKeysListEntry> data;
public ImportKeysAdapter(Activity activity) {
super(activity, -1);
mActivity = activity;
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@SuppressLint("NewApi")
public void setData(List<ImportKeysListEntry> data) {
clear();
if (data != null) {
this.data = data;
// add data to extended ArrayAdapter
if (Build.VERSION.SDK_INT >= 11) {
addAll(data);
} else {
for (ImportKeysListEntry entry : data) {
add(entry);
}
}
}
}
public List<ImportKeysListEntry> getData() {
return data;
}
@Override
public boolean hasStableIds() {
return true;
}
public View getView(int position, View convertView, ViewGroup parent) {
ImportKeysListEntry entry = data.get(position);
View view = mInflater.inflate(R.layout.import_keys_list_entry, null);
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
mainUserId.setText(R.string.unknown_user_id);
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
mainUserIdRest.setText("");
TextView keyId = (TextView) view.findViewById(R.id.keyId);
keyId.setText(R.string.no_key);
TextView fingerprint = (TextView) view.findViewById(R.id.fingerprint);
TextView algorithm = (TextView) view.findViewById(R.id.algorithm);
algorithm.setText("");
TextView status = (TextView) view.findViewById(R.id.status);
status.setText("");
String userId = entry.userIds.get(0);
if (userId != null) {
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
// show red user id if it is a secret key
if (entry.secretKey) {
userId = mActivity.getString(R.string.secret_key) + " " + userId;
mainUserId.setTextColor(Color.RED);
} else {
mainUserId.setText(userIdSplit[0]);
}
}
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
mainUserIdRest.setText(userIdSplit[1]);
mainUserIdRest.setVisibility(View.VISIBLE);
} else {
mainUserIdRest.setVisibility(View.GONE);
}
}
keyId.setText(entry.hexKeyId);
fingerprint.setText(mActivity.getString(R.string.fingerprint) + " " + entry.fingerPrint);
algorithm.setText("" + entry.bitStrength + "/" + entry.algorithm);
if (entry.revoked) {
status.setText("revoked");
} else {
status.setVisibility(View.GONE);
}
LinearLayout ll = (LinearLayout) view.findViewById(R.id.list);
if (entry.userIds.size() == 1) {
ll.setVisibility(View.GONE);
} else {
boolean first = true;
boolean second = true;
for (String uid : entry.userIds) {
if (first) {
first = false;
continue;
}
if (!second) {
View sep = new View(mActivity);
sep.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark);
ll.addView(sep);
}
TextView uidView = (TextView) mInflater.inflate(
R.layout.import_keys_list_entry_user_id, null);
uidView.setText(uid);
ll.addView(uidView);
second = false;
}
}
CheckBox cBox = (CheckBox) view.findViewById(R.id.selected);
cBox.setChecked(entry.isSelected());
return view;
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import java.io.Serializable;
import java.util.ArrayList;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.util.AlgorithmNames;
import org.sufficientlysecure.keychain.util.IterableIterator;
public class ImportKeysListEntry implements Serializable {
private static final long serialVersionUID = -7797972103284992662L;
public ArrayList<String> userIds;
public long keyId;
public boolean revoked;
// public Date date;
public String fingerPrint;
public String hexKeyId;
public int bitStrength;
public String algorithm;
public boolean secretKey;
AlgorithmNames algorithmNames;
private boolean selected;
/**
* Constructor for later querying from keyserver
*/
public ImportKeysListEntry() {
secretKey = false;
userIds = new ArrayList<String>();
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
/**
* Constructor based on key object, used for import from NFC, QR Codes, files
*
* @param pgpKey
*/
@SuppressWarnings("unchecked")
public ImportKeysListEntry(PGPKeyRing pgpKeyRing) {
// selected is default
this.selected = true;
if (pgpKeyRing instanceof PGPSecretKeyRing) {
secretKey = true;
} else {
secretKey = false;
}
userIds = new ArrayList<String>();
for (String userId : new IterableIterator<String>(pgpKeyRing.getPublicKey().getUserIDs())) {
userIds.add(userId);
}
this.keyId = pgpKeyRing.getPublicKey().getKeyID();
this.revoked = pgpKeyRing.getPublicKey().isRevoked();
this.fingerPrint = PgpKeyHelper.convertFingerprintToHex(pgpKeyRing.getPublicKey()
.getFingerprint());
this.hexKeyId = PgpKeyHelper.convertKeyIdToHex(keyId);
this.bitStrength = pgpKeyRing.getPublicKey().getBitStrength();
int algorithm = pgpKeyRing.getPublicKey().getAlgorithm();
if (algorithm == PGPPublicKey.RSA_ENCRYPT || algorithm == PGPPublicKey.RSA_GENERAL
|| algorithm == PGPPublicKey.RSA_SIGN) {
this.algorithm = "RSA";
} else if (algorithm == PGPPublicKey.DSA) {
this.algorithm = "DSA";
} else if (algorithm == PGPPublicKey.ELGAMAL_ENCRYPT
|| algorithm == PGPPublicKey.ELGAMAL_GENERAL) {
this.algorithm = "ElGamal";
} else if (algorithm == PGPPublicKey.EC || algorithm == PGPPublicKey.ECDSA) {
this.algorithm = "ECC";
} else {
// TODO: with resources
this.algorithm = "unknown";
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPObjectFactory;
import org.spongycastle.openpgp.PGPUtil;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
public class ImportKeysListLoader extends AsyncTaskLoader<List<ImportKeysListEntry>> {
Context mContext;
InputData mInputData;
ArrayList<ImportKeysListEntry> data = new ArrayList<ImportKeysListEntry>();
public ImportKeysListLoader(Context context, InputData inputData) {
super(context);
this.mContext = context;
this.mInputData = inputData;
}
@Override
public List<ImportKeysListEntry> loadInBackground() {
if (mInputData == null) {
Log.e(Constants.TAG, "Input data is null!");
return data;
}
generateListOfKeyrings(mInputData);
return data;
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
protected void onStopLoading() {
cancelLoad();
}
@Override
public void deliverResult(List<ImportKeysListEntry> data) {
super.deliverResult(data);
}
/**
* Reads all PGPKeyRing objects from input
*
* @param keyringBytes
* @return
*/
private void generateListOfKeyrings(InputData inputData) {
PositionAwareInputStream progressIn = new PositionAwareInputStream(
inputData.getInputStream());
// need to have access to the bufferedInput, so we can reuse it for the possible
// PGPObject chunks after the first one, e.g. files with several consecutive ASCII
// armour blocks
BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
try {
// read all available blocks... (asc files can contain many blocks with BEGIN END)
while (bufferedInput.available() > 0) {
InputStream in = PGPUtil.getDecoderStream(bufferedInput);
PGPObjectFactory objectFactory = new PGPObjectFactory(in);
// go through all objects in this block
Object obj;
while ((obj = objectFactory.nextObject()) != null) {
Log.d(Constants.TAG, "Found class: " + obj.getClass());
if (obj instanceof PGPKeyRing) {
PGPKeyRing newKeyring = (PGPKeyRing) obj;
addToData(newKeyring);
} else {
Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
}
}
}
} catch (Exception e) {
Log.e(Constants.TAG, "Exception on parsing key file!", e);
}
}
private void addToData(PGPKeyRing keyring) {
ImportKeysListEntry item = new ImportKeysListEntry(keyring);
data.add(item);
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import java.util.HashMap;
import java.util.Set;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import org.sufficientlysecure.keychain.util.Log;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
/**
* Implements StickyListHeadersAdapter from library
*/
public class KeyListPublicAdapter extends CursorAdapter implements StickyListHeadersAdapter {
private LayoutInflater mInflater;
private int mSectionColumnIndex;
private int mIndexUserId;
@SuppressLint("UseSparseArrays")
private HashMap<Integer, Boolean> mSelection = new HashMap<Integer, Boolean>();
public KeyListPublicAdapter(Context context, Cursor c, int flags, int sectionColumnIndex) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
mSectionColumnIndex = sectionColumnIndex;
initIndex(c);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
initIndex(newCursor);
return super.swapCursor(newCursor);
}
/**
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
* performance comparison see http://stackoverflow.com/a/17999582
*
* @param cursor
*/
private void initIndex(Cursor cursor) {
if (cursor != null) {
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
}
}
/**
* Bind cursor data to the item list view
*
* NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. Thus
* no ViewHolder is required here.
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
mainUserId.setText(R.string.unknown_user_id);
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
mainUserIdRest.setText("");
String userId = cursor.getString(mIndexUserId);
if (userId != null) {
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
mainUserId.setText(userIdSplit[0]);
}
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
mainUserIdRest.setText(userIdSplit[1]);
}
}
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(R.layout.key_list_item, null);
}
/**
* Creates a new header view and binds the section headers to it. It uses the ViewHolder
* pattern. Most functionality is similar to getView() from Android's CursorAdapter.
*
* NOTE: The variables mDataValid and mCursor are available due to the super class
* CursorAdapter.
*/
@Override
public View getHeaderView(int position, View convertView, ViewGroup parent) {
HeaderViewHolder holder;
if (convertView == null) {
holder = new HeaderViewHolder();
convertView = mInflater.inflate(R.layout.key_list_public_header, parent, false);
holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text);
convertView.setTag(holder);
} else {
holder = (HeaderViewHolder) convertView.getTag();
}
if (!mDataValid) {
// no data available at this point
Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
return convertView;
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
// set header text as first char in user id
String userId = mCursor.getString(mSectionColumnIndex);
String headerText = convertView.getResources().getString(R.string.unknown_user_id);
if (userId != null && userId.length() > 0) {
headerText = "" + mCursor.getString(mSectionColumnIndex).subSequence(0, 1).charAt(0);
}
holder.text.setText(headerText);
return convertView;
}
/**
* Header IDs should be static, position=1 should always return the same Id that is.
*/
@Override
public long getHeaderId(int position) {
if (!mDataValid) {
// no data available at this point
Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
return -1;
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
// return the first character of the name as ID because this is what
// headers private HashMap<Integer, Boolean> mSelection = new HashMap<Integer,
// Boolean>();are based upon
String userId = mCursor.getString(mSectionColumnIndex);
if (userId != null && userId.length() > 0) {
return userId.subSequence(0, 1).charAt(0);
} else {
return Long.MAX_VALUE;
}
}
class HeaderViewHolder {
TextView text;
}
/** -------------------------- MULTI-SELECTION METHODS -------------- */
public void setNewSelection(int position, boolean value) {
mSelection.put(position, value);
notifyDataSetChanged();
}
public boolean isPositionChecked(int position) {
Boolean result = mSelection.get(position);
return result == null ? false : result;
}
public Set<Integer> getCurrentCheckedPosition() {
return mSelection.keySet();
}
public void removeSelection(int position) {
mSelection.remove(position);
notifyDataSetChanged();
}
public void clearSelection() {
mSelection.clear();
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// let the adapter handle setting up the row views
View v = super.getView(position, convertView, parent);
/**
* Change color for multi-selection
*/
// default color
v.setBackgroundColor(Color.TRANSPARENT);
if (mSelection.get(position) != null) {
// this is a selected position, change color!
v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis));
}
return v;
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import java.util.HashMap;
import java.util.Set;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class KeyListSecretAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private int mIndexUserId;
@SuppressLint("UseSparseArrays")
private HashMap<Integer, Boolean> mSelection = new HashMap<Integer, Boolean>();
public KeyListSecretAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
initIndex(c);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
initIndex(newCursor);
return super.swapCursor(newCursor);
}
/**
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
* performance comparison see http://stackoverflow.com/a/17999582
*
* @param cursor
*/
private void initIndex(Cursor cursor) {
if (cursor != null) {
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
}
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
mainUserId.setText(R.string.unknown_user_id);
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
mainUserIdRest.setText("");
String userId = cursor.getString(mIndexUserId);
if (userId != null) {
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
mainUserId.setText(userIdSplit[0]);
}
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
mainUserIdRest.setText(userIdSplit[1]);
}
}
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(R.layout.key_list_item, null);
}
/** -------------------------- MULTI-SELECTION METHODS -------------- */
public void setNewSelection(int position, boolean value) {
mSelection.put(position, value);
notifyDataSetChanged();
}
public boolean isPositionChecked(int position) {
Boolean result = mSelection.get(position);
return result == null ? false : result;
}
public Set<Integer> getCurrentCheckedPosition() {
return mSelection.keySet();
}
public void removeSelection(int position) {
mSelection.remove(position);
notifyDataSetChanged();
}
public void clearSelection() {
mSelection.clear();
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// let the adapter handle setting up the row views
View v = super.getView(position, convertView, parent);
/**
* Change color for multi-selection
*/
// default color
v.setBackgroundColor(Color.TRANSPARENT);
if (mSelection.get(position) != null) {
// this is a selected position, change color!
v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis));
}
return v;
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import android.content.Context;
import android.widget.ArrayAdapter;
public class KeyValueSpinnerAdapter extends ArrayAdapter<String> {
private final HashMap<Integer, String> mData;
private final int[] mKeys;
private final String[] mValues;
static <K, V extends Comparable<? super V>> SortedSet<Map.Entry<K, V>> entriesSortedByValues(
Map<K, V> map) {
SortedSet<Map.Entry<K, V>> sortedEntries = new TreeSet<Map.Entry<K, V>>(
new Comparator<Map.Entry<K, V>>() {
@Override
public int compare(Map.Entry<K, V> e1, Map.Entry<K, V> e2) {
return e1.getValue().compareTo(e2.getValue());
}
});
sortedEntries.addAll(map.entrySet());
return sortedEntries;
}
public KeyValueSpinnerAdapter(Context context, HashMap<Integer, String> objects) {
// To make the drop down a simple text box
super(context, android.R.layout.simple_spinner_item);
mData = objects;
// To make the drop down view a radio button list
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
SortedSet<Map.Entry<Integer, String>> sorted = entriesSortedByValues(objects);
// Assign hash keys with a position so that we can present and retrieve them
int i = 0;
mKeys = new int[mData.size()];
mValues = new String[mData.size()];
for (Map.Entry<Integer, String> entry : sorted) {
mKeys[i] = entry.getKey();
mValues[i] = entry.getValue();
i++;
}
}
public int getCount() {
return mData.size();
}
/**
* Returns the value
*/
@Override
public String getItem(int position) {
// return the value based on the position. This is displayed in the list.
return mValues[position];
}
/**
* Returns item key
*/
public long getItemId(int position) {
// Return an id to represent the item.
return mKeys[position];
}
/**
* Find position from key
*/
public int getPosition(long itemId) {
for (int i = 0; i < mKeys.length; i++) {
if ((int) itemId == mKeys[i]) {
return i;
}
}
return -1;
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.TextView;
public class SelectKeyCursorAdapter extends CursorAdapter {
protected int mKeyType;
private LayoutInflater mInflater;
private ListView mListView;
private int mIndexUserId;
private int mIndexMasterKeyId;
private int mIndexProjectionValid;
private int mIndexProjectionAvailable;
public final static String PROJECTION_ROW_AVAILABLE = "available";
public final static String PROJECTION_ROW_VALID = "valid";
public SelectKeyCursorAdapter(Context context, Cursor c, int flags, ListView listView,
int keyType) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
mListView = listView;
mKeyType = keyType;
initIndex(c);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
initIndex(newCursor);
return super.swapCursor(newCursor);
}
/**
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
* performance comparison see http://stackoverflow.com/a/17999582
*
* @param cursor
*/
private void initIndex(Cursor cursor) {
if (cursor != null) {
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
mIndexMasterKeyId = cursor.getColumnIndexOrThrow(KeyRings.MASTER_KEY_ID);
mIndexProjectionValid = cursor.getColumnIndexOrThrow(PROJECTION_ROW_VALID);
mIndexProjectionAvailable = cursor.getColumnIndexOrThrow(PROJECTION_ROW_AVAILABLE);
}
}
public String getUserId(int position) {
mCursor.moveToPosition(position);
return mCursor.getString(mIndexUserId);
}
public long getMasterKeyId(int position) {
mCursor.moveToPosition(position);
return mCursor.getLong(mIndexMasterKeyId);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
boolean valid = cursor.getInt(mIndexProjectionValid) > 0;
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
mainUserId.setText(R.string.unknown_user_id);
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
mainUserIdRest.setText("");
TextView keyId = (TextView) view.findViewById(R.id.keyId);
keyId.setText(R.string.no_key);
TextView status = (TextView) view.findViewById(R.id.status);
status.setText(R.string.unknown_status);
String userId = cursor.getString(mIndexUserId);
if (userId != null) {
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
mainUserId.setText(userIdSplit[0]);
}
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
mainUserIdRest.setText(userIdSplit[1]);
}
}
long masterKeyId = cursor.getLong(mIndexMasterKeyId);
keyId.setText(PgpKeyHelper.convertKeyIdToHex(masterKeyId));
if (valid) {
if (mKeyType == Id.type.public_key) {
status.setText(R.string.can_encrypt);
} else {
status.setText(R.string.can_sign);
}
} else {
if (cursor.getInt(mIndexProjectionAvailable) > 0) {
// has some CAN_ENCRYPT keys, but col(ROW_VALID) = 0, so must be revoked or
// expired
status.setText(R.string.expired);
} else {
status.setText(R.string.no_key);
}
}
CheckBox selected = (CheckBox) view.findViewById(R.id.selected);
if (mKeyType == Id.type.public_key) {
selected.setVisibility(View.VISIBLE);
if (!valid) {
mListView.setItemChecked(cursor.getPosition(), false);
}
selected.setChecked(mListView.isItemChecked(cursor.getPosition()));
selected.setEnabled(valid);
} else {
selected.setVisibility(View.GONE);
}
status.setText(status.getText() + " ");
view.setEnabled(valid);
mainUserId.setEnabled(valid);
mainUserIdRest.setEnabled(valid);
keyId.setEnabled(valid);
status.setEnabled(valid);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(R.layout.select_key_item, null);
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class ViewKeyKeysAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private int mIndexKeyId;
private int mIndexAlgorithm;
private int mIndexKeySize;
private int mIndexIsMasterKey;
private int mIndexCanCertify;
private int mIndexCanEncrypt;
private int mIndexCanSign;
public ViewKeyKeysAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
initIndex(c);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
initIndex(newCursor);
return super.swapCursor(newCursor);
}
/**
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
* performance comparison see http://stackoverflow.com/a/17999582
*
* @param cursor
*/
private void initIndex(Cursor cursor) {
if (cursor != null) {
mIndexKeyId = cursor.getColumnIndexOrThrow(Keys.KEY_ID);
mIndexAlgorithm = cursor.getColumnIndexOrThrow(Keys.ALGORITHM);
mIndexKeySize = cursor.getColumnIndexOrThrow(Keys.KEY_SIZE);
mIndexIsMasterKey = cursor.getColumnIndexOrThrow(Keys.IS_MASTER_KEY);
mIndexCanCertify = cursor.getColumnIndexOrThrow(Keys.CAN_CERTIFY);
mIndexCanEncrypt = cursor.getColumnIndexOrThrow(Keys.CAN_ENCRYPT);
mIndexCanSign = cursor.getColumnIndexOrThrow(Keys.CAN_SIGN);
}
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
String keyIdStr = "0x" + PgpKeyHelper.convertKeyIdToHex(cursor.getLong(mIndexKeyId));
String algorithmStr = PgpKeyHelper.getAlgorithmInfo(cursor.getInt(mIndexAlgorithm),
cursor.getInt(mIndexKeySize));
TextView keyId = (TextView) view.findViewById(R.id.keyId);
keyId.setText(keyIdStr);
TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails);
keyDetails.setText("(" + algorithmStr + ")");
ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey);
if (cursor.getInt(mIndexIsMasterKey) != 1) {
masterKeyIcon.setVisibility(View.INVISIBLE);
} else {
masterKeyIcon.setVisibility(View.VISIBLE);
}
ImageView certifyIcon = (ImageView) view.findViewById(R.id.ic_certifyKey);
if (cursor.getInt(mIndexCanCertify) != 1) {
certifyIcon.setVisibility(View.GONE);
} else {
certifyIcon.setVisibility(View.VISIBLE);
}
ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey);
if (cursor.getInt(mIndexCanEncrypt) != 1) {
encryptIcon.setVisibility(View.GONE);
} else {
encryptIcon.setVisibility(View.VISIBLE);
}
ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey);
if (cursor.getInt(mIndexCanSign) != 1) {
signIcon.setVisibility(View.GONE);
} else {
signIcon.setVisibility(View.VISIBLE);
}
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(R.layout.view_key_keys_item, null);
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class ViewKeyUserIdsAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private int mIndexUserId;
public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
initIndex(c);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
initIndex(newCursor);
return super.swapCursor(newCursor);
}
/**
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
* performance comparison see http://stackoverflow.com/a/17999582
*
* @param cursor
*/
private void initIndex(Cursor cursor) {
if (cursor != null) {
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
}
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
String userIdStr = cursor.getString(mIndexUserId);
TextView userId = (TextView) view.findViewById(R.id.userId);
userId.setText(userIdStr);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(R.layout.view_key_userids_item, null);
}
}

Some files were not shown because too many files have changed in this diff Show More