Merge pull request #2351 from open-keychain/sql-delight
Use SqlDelight in favor of ContentProviders
This commit is contained in:
@@ -38,8 +38,7 @@ import com.nispok.snackbar.Snackbar;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
|
||||
@@ -53,7 +52,7 @@ import static org.hamcrest.CoreMatchers.endsWith;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSnackbarLineColor;
|
||||
|
||||
|
||||
public class TestHelpers {
|
||||
public class AndroidTestHelpers {
|
||||
|
||||
public static void dismissSnackbar() {
|
||||
onView(withClassName(endsWith("Snackbar")))
|
||||
@@ -152,7 +151,7 @@ public class TestHelpers {
|
||||
|
||||
public static void cleanupForTests(Context context) throws Exception {
|
||||
|
||||
new KeychainDatabase(context).clearDatabase();
|
||||
KeychainDatabase.getInstance(context).clearDatabase();
|
||||
|
||||
// import these two, make sure they're there
|
||||
importKeysFromResource(context, "x.sec.asc");
|
||||
@@ -161,5 +160,4 @@ public class TestHelpers {
|
||||
PassphraseCacheService.clearCachedPassphrases(context);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,8 +27,7 @@ import android.view.View;
|
||||
import com.tokenautocomplete.TokenCompleteTextView;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
|
||||
import static android.support.test.InstrumentationRegistry.getTargetContext;
|
||||
|
||||
|
||||
@@ -28,13 +28,9 @@ import android.widget.ViewAnimator;
|
||||
|
||||
import com.nispok.snackbar.Snackbar;
|
||||
|
||||
import org.hamcrest.BaseMatcher;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView;
|
||||
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
@@ -90,15 +86,14 @@ public abstract class CustomMatchers {
|
||||
}
|
||||
|
||||
public static Matcher<RecyclerView.ViewHolder> withKeyHolderId(final long keyId) {
|
||||
return new BoundedMatcher<RecyclerView.ViewHolder, KeySectionedListAdapter.KeyItemViewHolder>
|
||||
(KeySectionedListAdapter.KeyItemViewHolder.class) {
|
||||
return new BoundedMatcher<RecyclerView.ViewHolder, RecyclerView.ViewHolder>(RecyclerView.ViewHolder.class) {
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("with ViewHolder id: " + keyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(KeySectionedListAdapter.KeyItemViewHolder item) {
|
||||
protected boolean matchesSafely(View item) {
|
||||
return item.getItemId() == keyId;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.rule.ServiceTestRule;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.test.suitebuilder.annotation.LargeTest;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -23,17 +22,14 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static android.support.test.espresso.Espresso.closeSoftKeyboard;
|
||||
import static android.support.test.espresso.Espresso.onData;
|
||||
import static android.support.test.espresso.Espresso.onView;
|
||||
import static android.support.test.espresso.action.ViewActions.click;
|
||||
import static android.support.test.espresso.action.ViewActions.typeText;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.cleanupForTests;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.cleanupForTests;
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
|
||||
@@ -32,7 +32,7 @@ import android.widget.AdapterView;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.TestHelpers;
|
||||
import org.sufficientlysecure.keychain.AndroidTestHelpers;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
|
||||
@@ -61,11 +61,11 @@ import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.hasItem;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.getImageNames;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.pickRandom;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.getImageNames;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.pickRandom;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild;
|
||||
@@ -95,7 +95,7 @@ public class AsymmetricFileOperationTests {
|
||||
public void setUp() throws Exception {
|
||||
Activity activity = mActivity.getActivity();
|
||||
|
||||
TestHelpers.copyFiles();
|
||||
AndroidTestHelpers.copyFiles();
|
||||
|
||||
// import these two, make sure they're there
|
||||
importKeysFromResource(activity, "x.sec.asc");
|
||||
|
||||
@@ -43,9 +43,9 @@ import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild;
|
||||
|
||||
@@ -20,37 +20,27 @@ package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.support.test.espresso.ViewAction;
|
||||
import android.support.test.espresso.action.ViewActions;
|
||||
import android.support.test.espresso.contrib.RecyclerViewActions;
|
||||
import android.support.test.espresso.matcher.ViewMatchers;
|
||||
import android.support.test.rule.ActivityTestRule;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Rule;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.matcher.CustomMatchers;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
|
||||
import static android.support.test.espresso.Espresso.onData;
|
||||
import static android.support.test.espresso.Espresso.onView;
|
||||
import static android.support.test.espresso.action.ViewActions.click;
|
||||
import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnHolderItem;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyHolderId;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId;
|
||||
|
||||
|
||||
//TODO This test is disabled because it needs to be fixed to work with updated code
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@@ -73,7 +63,7 @@ public class EditKeyTest {
|
||||
public void test01Edit() throws Exception {
|
||||
Activity activity = mActivity.getActivity();
|
||||
|
||||
new KeychainDatabase(activity).clearDatabase();
|
||||
KeychainDatabase.getInstance(activity).clearDatabase();
|
||||
|
||||
// import key for testing, get a stable initial state
|
||||
importKeysFromResource(activity, "x.sec.asc");
|
||||
|
||||
@@ -27,18 +27,15 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.support.test.espresso.contrib.RecyclerViewActions;
|
||||
import android.support.test.espresso.intent.Intents;
|
||||
import android.support.test.espresso.intent.rule.IntentsTestRule;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.TestHelpers;
|
||||
import org.sufficientlysecure.keychain.matcher.CustomMatchers;
|
||||
import org.sufficientlysecure.keychain.AndroidTestHelpers;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
@@ -46,7 +43,6 @@ import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import java.io.File;
|
||||
|
||||
import static android.support.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static android.support.test.espresso.Espresso.onData;
|
||||
import static android.support.test.espresso.Espresso.onView;
|
||||
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
|
||||
import static android.support.test.espresso.Espresso.pressBack;
|
||||
@@ -67,16 +63,15 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.hasItem;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.dismissSnackbar;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.getImageNames;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.pickRandom;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.dismissSnackbar;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.getImageNames;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.pickRandom;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyHolderId;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId;
|
||||
import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable;
|
||||
|
||||
//TODO This test is disabled because it needs to be fixed to work with updated code
|
||||
@@ -104,7 +99,7 @@ public class MiscCryptOperationTests {
|
||||
|
||||
mActivity = mActivityRule.getActivity();
|
||||
|
||||
TestHelpers.copyFiles();
|
||||
AndroidTestHelpers.copyFiles();
|
||||
|
||||
// import these two, make sure they're there
|
||||
importKeysFromResource(mActivity, "x.sec.asc");
|
||||
|
||||
@@ -52,8 +52,8 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withEncryptionStatus;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSignatureNone;
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Vincent Breitmoser <look@my.amazin.horse>
|
||||
*
|
||||
* 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 android.app.Activity;
|
||||
import android.app.Instrumentation.ActivityResult;
|
||||
import android.content.Intent;
|
||||
import android.support.test.espresso.intent.rule.IntentsTestRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Rule;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
|
||||
import static android.support.test.espresso.Espresso.onView;
|
||||
import static android.support.test.espresso.action.ViewActions.click;
|
||||
import static android.support.test.espresso.intent.Intents.intending;
|
||||
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction;
|
||||
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra;
|
||||
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasType;
|
||||
import static android.support.test.espresso.intent.matcher.UriMatchers.hasHost;
|
||||
import static android.support.test.espresso.intent.matcher.UriMatchers.hasScheme;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.startsWith;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.checkAndDismissSnackbar;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.cleanupForTests;
|
||||
|
||||
//TODO This test is disabled because it needs to be fixed to work with updated code
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
//@RunWith(AndroidJUnit4.class)
|
||||
//@LargeTest
|
||||
public class ViewKeyAdvShareTest {
|
||||
|
||||
@Rule
|
||||
public final IntentsTestRule<ViewKeyAdvActivity> mActivityRule
|
||||
= new IntentsTestRule<ViewKeyAdvActivity>(ViewKeyAdvActivity.class) {
|
||||
@Override
|
||||
protected Intent getActivityIntent() {
|
||||
Intent intent = super.getActivityIntent();
|
||||
intent.setData(KeyRings.buildGenericKeyRingUri(0x9D604D2F310716A3L));
|
||||
intent.putExtra(ViewKeyAdvActivity.EXTRA_SELECTED_TAB, ViewKeyAdvActivity.TAB_SHARE);
|
||||
return intent;
|
||||
}
|
||||
};
|
||||
private Activity mActivity;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mActivity = mActivityRule.getActivity();
|
||||
|
||||
cleanupForTests(mActivity);
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void testShareOperations() throws Exception {
|
||||
|
||||
// no-op should yield snackbar
|
||||
onView(withId(R.id.view_key_action_fingerprint_clipboard)).perform(click());
|
||||
checkAndDismissSnackbar(Style.OK, R.string.fingerprint_copied_to_clipboard);
|
||||
assertThat("clipboard data is fingerprint", ClipboardReflection.getClipboardText(mActivity),
|
||||
is("c619d53f7a5f96f391a84ca79d604d2f310716a3"));
|
||||
|
||||
intending(allOf(
|
||||
hasAction("android.intent.action.CHOOSER"),
|
||||
hasExtra(equalTo(Intent.EXTRA_INTENT), allOf(
|
||||
hasAction(Intent.ACTION_SEND),
|
||||
hasType("text/plain"),
|
||||
hasExtra(is(Intent.EXTRA_TEXT), is("openpgp4fpr:c619d53f7a5f96f391a84ca79d604d2f310716a3")),
|
||||
hasExtra(is(Intent.EXTRA_STREAM),
|
||||
allOf(hasScheme("content"), hasHost(TemporaryFileProvider.AUTHORITY)))
|
||||
))
|
||||
)).respondWith(new ActivityResult(Activity.RESULT_OK, null));
|
||||
onView(withId(R.id.view_key_action_fingerprint_share)).perform(click());
|
||||
|
||||
onView(withId(R.id.view_key_action_key_clipboard)).perform(click());
|
||||
checkAndDismissSnackbar(Style.OK, R.string.key_copied_to_clipboard);
|
||||
assertThat("clipboard data is key",
|
||||
ClipboardReflection.getClipboardText(mActivity), startsWith("----"));
|
||||
|
||||
intending(allOf(
|
||||
hasAction("android.intent.action.CHOOSER"),
|
||||
hasExtra(equalTo(Intent.EXTRA_INTENT), allOf(
|
||||
hasAction(Intent.ACTION_SEND),
|
||||
hasType("text/plain"),
|
||||
hasExtra(is(Intent.EXTRA_TEXT), startsWith("----")),
|
||||
hasExtra(is(Intent.EXTRA_STREAM),
|
||||
allOf(hasScheme("content"), hasHost(TemporaryFileProvider.AUTHORITY)))
|
||||
))
|
||||
)).respondWith(new ActivityResult(Activity.RESULT_OK, null));
|
||||
onView(withId(R.id.view_key_action_key_share)).perform(click());
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource;
|
||||
import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId;
|
||||
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyToken;
|
||||
|
||||
36
OpenKeychain/src/debug/res/xml/shortcuts.xml
Normal file
36
OpenKeychain/src/debug/res/xml/shortcuts.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_comment_text_grey600_24dp"
|
||||
android:shortcutId="encrypt_text"
|
||||
android:shortcutLongLabel="@string/btn_encrypt_text"
|
||||
android:shortcutShortLabel="@string/btn_encrypt_text">
|
||||
<intent
|
||||
android:action="org.sufficientlysecure.keychain.action.ENCRYPT_FILES"
|
||||
android:targetClass="org.sufficientlysecure.keychain.ui.EncryptTextActivity"
|
||||
android:targetPackage="org.sufficientlysecure.keychain.debug" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_folder_grey_24dp"
|
||||
android:shortcutId="encrypt_files"
|
||||
android:shortcutLongLabel="@string/btn_encrypt_files"
|
||||
android:shortcutShortLabel="@string/btn_encrypt_files">
|
||||
<intent
|
||||
android:action="org.sufficientlysecure.keychain.action.ENCRYPT_TEXT"
|
||||
android:targetClass="org.sufficientlysecure.keychain.ui.EncryptFilesActivity"
|
||||
android:targetPackage="org.sufficientlysecure.keychain.debug" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/status_signature_unverified_cutout_24dp"
|
||||
android:shortcutId="debug_actions"
|
||||
android:shortcutLongLabel="@string/shortcut_debug"
|
||||
android:shortcutShortLabel="@string/shortcut_debug">
|
||||
<intent
|
||||
android:action="org.sufficientlysecure.keychain.debug"
|
||||
android:targetClass="org.sufficientlysecure.keychain.ui.DebugActionsActivity"
|
||||
android:targetPackage="org.sufficientlysecure.keychain.debug" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -95,15 +95,6 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Keychain.Light">
|
||||
<!-- broadcast receiver for Wi-Fi Connection -->
|
||||
<receiver
|
||||
android:name=".network.NetworkReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- broadcast receiver for Orbots status -->
|
||||
<receiver android:name=".network.orbot.OrbotStatusReceiver">
|
||||
<intent-filter>
|
||||
@@ -128,6 +119,8 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<!-- singleTop for NFC dispatch, see SecurityTokenOperationActivity -->
|
||||
<activity
|
||||
@@ -177,15 +170,6 @@
|
||||
<data android:mimeType="vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.ViewCertActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/title_view_cert"
|
||||
android:parentActivityName=".ui.keyview.ViewKeyActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.keyview.ViewKeyActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SafeSlingerActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
@@ -983,6 +967,12 @@
|
||||
android:name=".remote.ui.RemoteDisplayTransferCodeActivity"
|
||||
android:theme="@style/Theme.Keychain.Transparent"/>
|
||||
|
||||
<activity android:name=".ui.DebugActionsActivity">
|
||||
<intent-filter>
|
||||
<action android:name="org.sufficientlysecure.keychain.debug" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Usb interceptor activity -->
|
||||
<activity
|
||||
android:name=".ui.UsbEventReceiverActivity"
|
||||
@@ -1070,21 +1060,6 @@
|
||||
android:resource="@xml/sync_adapter_contacts_structure" />
|
||||
</service>
|
||||
|
||||
<!-- keyserver sync service -->
|
||||
<service
|
||||
android:name=".service.KeyserverSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_adapter_keys" />
|
||||
</service>
|
||||
|
||||
<!-- Storage Provider for temporary decrypted files.
|
||||
For security considerations, read class! -->
|
||||
<provider
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v4.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Animation.AnimationListener;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Transformation;
|
||||
import android.widget.AbsListView;
|
||||
|
||||
|
||||
/**
|
||||
* Same as SwipeRefreshLayout, only updateContentOffsetTop and REFRESH_TRIGGER_DISTANCE
|
||||
* have been modified!
|
||||
*/
|
||||
public class NoScrollableSwipeRefreshLayout extends ViewGroup {
|
||||
private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
|
||||
private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f;
|
||||
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
|
||||
private static final float PROGRESS_BAR_HEIGHT = 4;
|
||||
private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f;
|
||||
private static final int REFRESH_TRIGGER_DISTANCE = 200;
|
||||
|
||||
private SwipeProgressBar mProgressBar; //the thing that shows progress is going
|
||||
private View mTarget; //the content that gets pulled down
|
||||
private int mOriginalOffsetTop;
|
||||
private OnRefreshListener mListener;
|
||||
private MotionEvent mDownEvent;
|
||||
private int mFrom;
|
||||
private boolean mRefreshing = false;
|
||||
private int mTouchSlop;
|
||||
private float mDistanceToTriggerSync = -1;
|
||||
private float mPrevY;
|
||||
private int mMediumAnimationDuration;
|
||||
private float mFromPercentage = 0;
|
||||
private float mCurrPercentage = 0;
|
||||
private int mProgressBarHeight;
|
||||
private int mCurrentTargetOffsetTop;
|
||||
// Target is returning to its start offset because it was cancelled or a
|
||||
// refresh was triggered.
|
||||
private boolean mReturningToStart;
|
||||
private final DecelerateInterpolator mDecelerateInterpolator;
|
||||
private final AccelerateInterpolator mAccelerateInterpolator;
|
||||
private static final int[] LAYOUT_ATTRS = new int[] {
|
||||
android.R.attr.enabled
|
||||
};
|
||||
|
||||
private final Animation mAnimateToStartPosition = new Animation() {
|
||||
@Override
|
||||
public void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
int targetTop = 0;
|
||||
if (mFrom != mOriginalOffsetTop) {
|
||||
targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime));
|
||||
}
|
||||
int offset = targetTop - mTarget.getTop();
|
||||
final int currentTop = mTarget.getTop();
|
||||
if (offset + currentTop < 0) {
|
||||
offset = 0 - currentTop;
|
||||
}
|
||||
setTargetOffsetTopAndBottom(offset);
|
||||
}
|
||||
};
|
||||
|
||||
private Animation mShrinkTrigger = new Animation() {
|
||||
@Override
|
||||
public void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime);
|
||||
mProgressBar.setTriggerPercentage(percent);
|
||||
}
|
||||
};
|
||||
|
||||
private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
// Once the target content has returned to its start position, reset
|
||||
// the target offset to 0
|
||||
mCurrentTargetOffsetTop = 0;
|
||||
}
|
||||
};
|
||||
|
||||
private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
mCurrPercentage = 0;
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable mReturnToStartPosition = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mReturningToStart = true;
|
||||
animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
|
||||
mReturnToStartPositionListener);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Cancel the refresh gesture and animate everything back to its original state.
|
||||
private final Runnable mCancel = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mReturningToStart = true;
|
||||
// Timeout fired since the user last moved their finger; animate the
|
||||
// trigger to 0 and put the target back at its original position
|
||||
if (mProgressBar != null) {
|
||||
mFromPercentage = mCurrPercentage;
|
||||
mShrinkTrigger.setDuration(mMediumAnimationDuration);
|
||||
mShrinkTrigger.setAnimationListener(mShrinkAnimationListener);
|
||||
mShrinkTrigger.reset();
|
||||
mShrinkTrigger.setInterpolator(mDecelerateInterpolator);
|
||||
startAnimation(mShrinkTrigger);
|
||||
}
|
||||
animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
|
||||
mReturnToStartPositionListener);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple constructor to use when creating a SwipeRefreshLayout from code.
|
||||
* @param context
|
||||
*/
|
||||
public NoScrollableSwipeRefreshLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor that is called when inflating SwipeRefreshLayout from XML.
|
||||
* @param context
|
||||
* @param attrs
|
||||
*/
|
||||
public NoScrollableSwipeRefreshLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
|
||||
mMediumAnimationDuration = getResources().getInteger(
|
||||
android.R.integer.config_mediumAnimTime);
|
||||
|
||||
setWillNotDraw(false);
|
||||
mProgressBar = new SwipeProgressBar(this);
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT);
|
||||
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
|
||||
mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR);
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
|
||||
setEnabled(a.getBoolean(0, true));
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
removeCallbacks(mCancel);
|
||||
removeCallbacks(mReturnToStartPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
removeCallbacks(mReturnToStartPosition);
|
||||
removeCallbacks(mCancel);
|
||||
}
|
||||
|
||||
private void animateOffsetToStartPosition(int from, AnimationListener listener) {
|
||||
mFrom = from;
|
||||
mAnimateToStartPosition.reset();
|
||||
mAnimateToStartPosition.setDuration(mMediumAnimationDuration);
|
||||
mAnimateToStartPosition.setAnimationListener(listener);
|
||||
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
|
||||
mTarget.startAnimation(mAnimateToStartPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listener to be notified when a refresh is triggered via the swipe
|
||||
* gesture.
|
||||
*/
|
||||
public void setOnRefreshListener(OnRefreshListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
private void setTriggerPercentage(float percent) {
|
||||
if (percent == 0f) {
|
||||
// No-op. A null trigger means it's uninitialized, and setting it to zero-percent
|
||||
// means we're trying to reset state, so there's nothing to reset in this case.
|
||||
mCurrPercentage = 0;
|
||||
return;
|
||||
}
|
||||
mCurrPercentage = percent;
|
||||
mProgressBar.setTriggerPercentage(percent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the widget that refresh state has changed. Do not call this when
|
||||
* refresh is triggered by a swipe gesture.
|
||||
*
|
||||
* @param refreshing Whether or not the view should show refresh progress.
|
||||
*/
|
||||
public void setRefreshing(boolean refreshing) {
|
||||
if (mRefreshing != refreshing) {
|
||||
ensureTarget();
|
||||
mCurrPercentage = 0;
|
||||
mRefreshing = refreshing;
|
||||
if (mRefreshing) {
|
||||
mProgressBar.start();
|
||||
} else {
|
||||
mProgressBar.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the four colors used in the progress animation. The first color will
|
||||
* also be the color of the bar that grows in response to a user swipe
|
||||
* gesture.
|
||||
*
|
||||
* @param colorRes1 Color resource.
|
||||
* @param colorRes2 Color resource.
|
||||
* @param colorRes3 Color resource.
|
||||
* @param colorRes4 Color resource.
|
||||
*/
|
||||
public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) {
|
||||
ensureTarget();
|
||||
final Resources res = getResources();
|
||||
final int color1 = res.getColor(colorRes1);
|
||||
final int color2 = res.getColor(colorRes2);
|
||||
final int color3 = res.getColor(colorRes3);
|
||||
final int color4 = res.getColor(colorRes4);
|
||||
mProgressBar.setColorScheme(color1, color2, color3,color4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the SwipeRefreshWidget is actively showing refresh
|
||||
* progress.
|
||||
*/
|
||||
public boolean isRefreshing() {
|
||||
return mRefreshing;
|
||||
}
|
||||
|
||||
private void ensureTarget() {
|
||||
// Don't bother getting the parent height if the parent hasn't been laid out yet.
|
||||
if (mTarget == null) {
|
||||
if (getChildCount() > 1 && !isInEditMode()) {
|
||||
throw new IllegalStateException(
|
||||
"SwipeRefreshLayout can host only one direct child");
|
||||
}
|
||||
mTarget = getChildAt(0);
|
||||
mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
|
||||
}
|
||||
if (mDistanceToTriggerSync == -1) {
|
||||
if (getParent() != null && ((View)getParent()).getHeight() > 0) {
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
mDistanceToTriggerSync = (int) Math.min(
|
||||
((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR,
|
||||
REFRESH_TRIGGER_DISTANCE * metrics.density);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
mProgressBar.draw(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
final int width = getMeasuredWidth();
|
||||
final int height = getMeasuredHeight();
|
||||
mProgressBar.setBounds(0, 0, width, mProgressBarHeight);
|
||||
if (getChildCount() == 0) {
|
||||
return;
|
||||
}
|
||||
final View child = getChildAt(0);
|
||||
final int childLeft = getPaddingLeft();
|
||||
final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
|
||||
final int childWidth = width - getPaddingLeft() - getPaddingRight();
|
||||
final int childHeight = height - getPaddingTop() - getPaddingBottom();
|
||||
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
if (getChildCount() > 1 && !isInEditMode()) {
|
||||
throw new IllegalStateException("SwipeRefreshLayout can host only one direct child");
|
||||
}
|
||||
if (getChildCount() > 0) {
|
||||
getChildAt(0).measure(
|
||||
MeasureSpec.makeMeasureSpec(
|
||||
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
|
||||
MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(
|
||||
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
|
||||
MeasureSpec.EXACTLY));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether it is possible for the child view of this layout to
|
||||
* scroll up. Override this if the child view is a custom view.
|
||||
*/
|
||||
public boolean canChildScrollUp() {
|
||||
if (android.os.Build.VERSION.SDK_INT < 14) {
|
||||
if (mTarget instanceof AbsListView) {
|
||||
final AbsListView absListView = (AbsListView) mTarget;
|
||||
return absListView.getChildCount() > 0
|
||||
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
|
||||
.getTop() < absListView.getPaddingTop());
|
||||
} else {
|
||||
return mTarget.getScrollY() > 0;
|
||||
}
|
||||
} else {
|
||||
return ViewCompat.canScrollVertically(mTarget, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
ensureTarget();
|
||||
boolean handled = false;
|
||||
if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
mReturningToStart = false;
|
||||
}
|
||||
if (isEnabled() && !mReturningToStart && !canChildScrollUp()) {
|
||||
handled = onTouchEvent(ev);
|
||||
}
|
||||
return !handled ? super.onInterceptTouchEvent(ev) : handled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestDisallowInterceptTouchEvent(boolean b) {
|
||||
// Nope.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
final int action = event.getAction();
|
||||
boolean handled = false;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mCurrPercentage = 0;
|
||||
mDownEvent = MotionEvent.obtain(event);
|
||||
mPrevY = mDownEvent.getY();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (mDownEvent != null && !mReturningToStart) {
|
||||
final float eventY = event.getY();
|
||||
float yDiff = eventY - mDownEvent.getY();
|
||||
if (yDiff > mTouchSlop) {
|
||||
// User velocity passed min velocity; trigger a refresh
|
||||
if (yDiff > mDistanceToTriggerSync) {
|
||||
// User movement passed distance; trigger a refresh
|
||||
startRefresh();
|
||||
handled = true;
|
||||
break;
|
||||
} else {
|
||||
// Just track the user's movement
|
||||
setTriggerPercentage(
|
||||
mAccelerateInterpolator.getInterpolation(
|
||||
yDiff / mDistanceToTriggerSync));
|
||||
float offsetTop = yDiff;
|
||||
if (mPrevY > eventY) {
|
||||
offsetTop = yDiff - mTouchSlop;
|
||||
}
|
||||
updateContentOffsetTop((int) (offsetTop));
|
||||
if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) {
|
||||
// If the user puts the view back at the top, we
|
||||
// don't need to. This shouldn't be considered
|
||||
// cancelling the gesture as the user can restart from the top.
|
||||
removeCallbacks(mCancel);
|
||||
} else {
|
||||
updatePositionTimeout();
|
||||
}
|
||||
mPrevY = event.getY();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (mDownEvent != null) {
|
||||
mDownEvent.recycle();
|
||||
mDownEvent = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
private void startRefresh() {
|
||||
removeCallbacks(mCancel);
|
||||
mReturnToStartPosition.run();
|
||||
setRefreshing(true);
|
||||
mListener.onRefresh();
|
||||
}
|
||||
|
||||
private void updateContentOffsetTop(int targetTop) {
|
||||
final int currentTop = mTarget.getTop();
|
||||
if (targetTop > mDistanceToTriggerSync) {
|
||||
targetTop = (int) mDistanceToTriggerSync;
|
||||
} else if (targetTop < 0) {
|
||||
targetTop = 0;
|
||||
}
|
||||
// setTargetOffsetTopAndBottom(targetTop - currentTop);
|
||||
}
|
||||
|
||||
private void setTargetOffsetTopAndBottom(int offset) {
|
||||
mTarget.offsetTopAndBottom(offset);
|
||||
mCurrentTargetOffsetTop = mTarget.getTop();
|
||||
}
|
||||
|
||||
private void updatePositionTimeout() {
|
||||
removeCallbacks(mCancel);
|
||||
postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classes that wish to be notified when the swipe gesture correctly
|
||||
* triggers a refresh should implement this interface.
|
||||
*/
|
||||
public interface OnRefreshListener {
|
||||
void onRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple AnimationListener to avoid having to implement unneeded methods in
|
||||
* AnimationListeners.
|
||||
*/
|
||||
private class BaseAnimationListener implements AnimationListener {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ public final class Constants {
|
||||
public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false;
|
||||
public static final boolean DEBUG_KEYSERVER_SYNC = false;
|
||||
|
||||
public static final boolean IS_RUNNING_UNITTEST = isRunningUnitTest();
|
||||
|
||||
public static final String TAG = DEBUG ? "Keychain D" : "Keychain";
|
||||
|
||||
public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain";
|
||||
@@ -108,9 +110,14 @@ public final class Constants {
|
||||
public static final File APP_DIR = new File(Environment.getExternalStorageDirectory(), "OpenKeychain");
|
||||
}
|
||||
|
||||
public static final class Notification {
|
||||
public static final class NotificationIds {
|
||||
public static final int PASSPHRASE_CACHE = 1;
|
||||
public static final int KEYSERVER_SYNC_FAIL_ORBOT = 2;
|
||||
public static final int KEYSERVER_SYNC = 3;
|
||||
}
|
||||
|
||||
public static final class NotificationChannels {
|
||||
public static final String KEYSERVER_SYNC = "keyserverSync";
|
||||
}
|
||||
|
||||
public static final class Pref {
|
||||
@@ -145,6 +152,7 @@ public final class Constants {
|
||||
public static final String SYNC_CONTACTS = "syncContacts";
|
||||
public static final String SYNC_KEYSERVER = "syncKeyserver";
|
||||
public static final String ENABLE_WIFI_SYNC_ONLY = "enableWifiSyncOnly";
|
||||
public static final String SYNC_IS_SCHEDULED = "syncIsScheduled";
|
||||
// other settings
|
||||
public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities";
|
||||
public static final String EXPERIMENTAL_ENABLE_KEYBASE = "experimentalEnableKeybase";
|
||||
@@ -205,4 +213,12 @@ public final class Constants {
|
||||
public static final KeyFormat SECURITY_TOKEN_V2_DEC = new RSAKeyFormat(2048, ELEN, RSAKeyFormat.RSAAlgorithmFormat.CRT_WITH_MODULUS);
|
||||
public static final KeyFormat SECURITY_TOKEN_V2_AUTH = new RSAKeyFormat(2048, ELEN, RSAKeyFormat.RSAAlgorithmFormat.CRT_WITH_MODULUS);
|
||||
|
||||
private static boolean isRunningUnitTest() {
|
||||
try {
|
||||
Class.forName("org.sufficientlysecure.keychain.KeychainTestRunner");
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,8 @@ import java.util.HashMap;
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.Application;
|
||||
import android.app.job.JobScheduler;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -38,8 +33,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.sufficientlysecure.keychain.network.TlsCertificatePinning;
|
||||
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
|
||||
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager;
|
||||
import org.sufficientlysecure.keychain.util.PRNGFixes;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import timber.log.Timber;
|
||||
@@ -95,26 +89,22 @@ public class KeychainApplication extends Application {
|
||||
if (preferences.isAppExecutedFirstTime()) {
|
||||
preferences.setAppExecutedFirstTime(false);
|
||||
|
||||
KeyserverSyncAdapterService.enableKeyserverSync(this);
|
||||
ContactSyncAdapterService.enableContactsSync(this);
|
||||
|
||||
preferences.setPrefVersionToCurrentVersion();
|
||||
}
|
||||
|
||||
if (Preferences.getKeyserverSyncEnabled(this)) {
|
||||
// will update a keyserver sync if the interval has changed
|
||||
KeyserverSyncAdapterService.updateInterval(this);
|
||||
}
|
||||
|
||||
// Upgrade preferences as needed
|
||||
preferences.upgradePreferences(this);
|
||||
preferences.upgradePreferences();
|
||||
|
||||
TlsCertificatePinning.addPinnedCertificate("hkps.pool.sks-keyservers.net", getAssets(), "hkps.pool.sks-keyservers.net.CA.cer");
|
||||
TlsCertificatePinning.addPinnedCertificate("pgp.mit.edu", getAssets(), "pgp.mit.edu.cer");
|
||||
TlsCertificatePinning.addPinnedCertificate("api.keybase.io", getAssets(), "api.keybase.io.CA.cer");
|
||||
TlsCertificatePinning.addPinnedCertificate("keyserver.ubuntu.com", getAssets(), "DigiCertGlobalRootCA.cer");
|
||||
|
||||
new Handler().postDelayed(() -> TemporaryFileProvider.cleanUp(getApplicationContext()), 1000);
|
||||
KeyserverSyncManager.updateKeyserverSyncSchedule(this, Constants.DEBUG_KEYSERVER_SYNC);
|
||||
|
||||
TemporaryFileProvider.scheduleCleanupImmediately();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
package org.sufficientlysecure.keychain;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
@@ -23,23 +23,20 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import android.arch.persistence.db.SupportSQLiteDatabase;
|
||||
import android.arch.persistence.db.SupportSQLiteOpenHelper;
|
||||
import android.arch.persistence.db.SupportSQLiteOpenHelper.Callback;
|
||||
import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration;
|
||||
import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.SQLException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.provider.BaseColumns;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAllowedKeysColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeerColumns;
|
||||
import org.sufficientlysecure.keychain.daos.LocalSecretKeyStorage;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeySignaturesColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.OverriddenWarnings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeysColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import timber.log.Timber;
|
||||
@@ -53,185 +50,86 @@ import timber.log.Timber;
|
||||
* - TEXT. The value is a text string, stored using the database encoding (UTF-8, UTF-16BE or UTF-16LE).
|
||||
* - BLOB. The value is a blob of data, stored exactly as it was input.
|
||||
*/
|
||||
public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
public class KeychainDatabase {
|
||||
private static final String DATABASE_NAME = "openkeychain.db";
|
||||
private static final int DATABASE_VERSION = 25;
|
||||
private Context mContext;
|
||||
private static final int DATABASE_VERSION = 29;
|
||||
private final SupportSQLiteOpenHelper supportSQLiteOpenHelper;
|
||||
|
||||
private static KeychainDatabase sInstance;
|
||||
|
||||
public static KeychainDatabase getInstance(Context context) {
|
||||
if (sInstance == null || Constants.IS_RUNNING_UNITTEST) {
|
||||
sInstance = new KeychainDatabase(context.getApplicationContext());
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public interface Tables {
|
||||
String KEY_RINGS_PUBLIC = "keyrings_public";
|
||||
String KEY_RINGS_SECRET = "keyrings_secret";
|
||||
String KEYS = "keys";
|
||||
String UPDATED_KEYS = "updated_keys";
|
||||
String KEY_SIGNATURES = "key_signatures";
|
||||
String USER_PACKETS = "user_packets";
|
||||
String CERTS = "certs";
|
||||
String API_APPS = "api_apps";
|
||||
String API_ALLOWED_KEYS = "api_allowed_keys";
|
||||
String OVERRIDDEN_WARNINGS = "overridden_warnings";
|
||||
String API_AUTOCRYPT_PEERS = "api_autocrypt_peers";
|
||||
}
|
||||
|
||||
private static final String CREATE_KEYRINGS_PUBLIC =
|
||||
"CREATE TABLE IF NOT EXISTS keyrings_public ("
|
||||
+ KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY,"
|
||||
+ KeyRingsColumns.KEY_RING_DATA + " BLOB"
|
||||
+ ")";
|
||||
private KeychainDatabase(Context context) {
|
||||
supportSQLiteOpenHelper =
|
||||
new FrameworkSQLiteOpenHelperFactory()
|
||||
.create(Configuration.builder(context).name(DATABASE_NAME).callback(
|
||||
new Callback(DATABASE_VERSION) {
|
||||
@Override
|
||||
public void onCreate(SupportSQLiteDatabase db) {
|
||||
KeychainDatabase.this.onCreate(db, context);
|
||||
}
|
||||
|
||||
private static final String CREATE_KEYRINGS_SECRET =
|
||||
"CREATE TABLE IF NOT EXISTS keyrings_secret ("
|
||||
+ KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY,"
|
||||
+ KeyRingsColumns.KEY_RING_DATA + " BLOB, "
|
||||
+ "FOREIGN KEY(" + KeyRingsColumns.MASTER_KEY_ID + ") "
|
||||
+ "REFERENCES keyrings_public(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
@Override
|
||||
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
KeychainDatabase.this.onUpgrade(db, context, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
private static final String CREATE_KEYS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " ("
|
||||
+ KeysColumns.MASTER_KEY_ID + " INTEGER, "
|
||||
+ KeysColumns.RANK + " INTEGER, "
|
||||
@Override
|
||||
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
KeychainDatabase.this.onDowngrade();
|
||||
}
|
||||
|
||||
+ KeysColumns.KEY_ID + " INTEGER, "
|
||||
+ KeysColumns.KEY_SIZE + " INTEGER, "
|
||||
+ KeysColumns.KEY_CURVE_OID + " TEXT, "
|
||||
+ KeysColumns.ALGORITHM + " INTEGER, "
|
||||
+ KeysColumns.FINGERPRINT + " BLOB, "
|
||||
|
||||
+ KeysColumns.CAN_CERTIFY + " INTEGER, "
|
||||
+ KeysColumns.CAN_SIGN + " INTEGER, "
|
||||
+ KeysColumns.CAN_ENCRYPT + " INTEGER, "
|
||||
+ KeysColumns.CAN_AUTHENTICATE + " INTEGER, "
|
||||
+ KeysColumns.IS_REVOKED + " INTEGER, "
|
||||
+ KeysColumns.HAS_SECRET + " INTEGER, "
|
||||
+ KeysColumns.IS_SECURE + " INTEGER, "
|
||||
|
||||
+ KeysColumns.CREATION + " INTEGER, "
|
||||
+ KeysColumns.EXPIRY + " INTEGER, "
|
||||
|
||||
+ "PRIMARY KEY(" + KeysColumns.MASTER_KEY_ID + ", " + KeysColumns.RANK + "),"
|
||||
+ "FOREIGN KEY(" + KeysColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_USER_PACKETS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.USER_PACKETS + "("
|
||||
+ UserPacketsColumns.MASTER_KEY_ID + " INTEGER, "
|
||||
+ UserPacketsColumns.TYPE + " INT, "
|
||||
+ UserPacketsColumns.USER_ID + " TEXT, "
|
||||
+ UserPacketsColumns.NAME + " TEXT, "
|
||||
+ UserPacketsColumns.EMAIL + " TEXT, "
|
||||
+ UserPacketsColumns.COMMENT + " TEXT, "
|
||||
+ UserPacketsColumns.ATTRIBUTE_DATA + " BLOB, "
|
||||
|
||||
+ UserPacketsColumns.IS_PRIMARY + " INTEGER, "
|
||||
+ UserPacketsColumns.IS_REVOKED + " INTEGER, "
|
||||
+ UserPacketsColumns.RANK+ " INTEGER, "
|
||||
|
||||
+ "PRIMARY KEY(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + "), "
|
||||
+ "FOREIGN KEY(" + UserPacketsColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_CERTS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.CERTS + "("
|
||||
+ CertsColumns.MASTER_KEY_ID + " INTEGER,"
|
||||
+ CertsColumns.RANK + " INTEGER, " // rank of certified uid
|
||||
|
||||
+ CertsColumns.KEY_ID_CERTIFIER + " INTEGER, " // certifying key
|
||||
+ CertsColumns.TYPE + " INTEGER, "
|
||||
+ CertsColumns.VERIFIED + " INTEGER, "
|
||||
+ CertsColumns.CREATION + " INTEGER, "
|
||||
|
||||
+ CertsColumns.DATA + " BLOB, "
|
||||
|
||||
+ "PRIMARY KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ", "
|
||||
+ CertsColumns.KEY_ID_CERTIFIER + "), "
|
||||
+ "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE,"
|
||||
+ "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ") REFERENCES "
|
||||
+ Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_UPDATE_KEYS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.UPDATED_KEYS + " ("
|
||||
+ UpdatedKeysColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY, "
|
||||
+ UpdatedKeysColumns.LAST_UPDATED + " INTEGER, "
|
||||
+ UpdatedKeysColumns.SEEN_ON_KEYSERVERS + " INTEGER, "
|
||||
+ "FOREIGN KEY(" + UpdatedKeysColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_KEY_SIGNATURES =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.KEY_SIGNATURES + " ("
|
||||
+ KeySignaturesColumns.MASTER_KEY_ID + " INTEGER NOT NULL, "
|
||||
+ KeySignaturesColumns.SIGNER_KEY_ID + " INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(" + KeySignaturesColumns.MASTER_KEY_ID + ", " + KeySignaturesColumns.SIGNER_KEY_ID + "), "
|
||||
+ "FOREIGN KEY(" + KeySignaturesColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_API_AUTOCRYPT_PEERS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.API_AUTOCRYPT_PEERS + " ("
|
||||
+ ApiAutocryptPeerColumns.PACKAGE_NAME + " TEXT NOT NULL, "
|
||||
+ ApiAutocryptPeerColumns.IDENTIFIER + " TEXT NOT NULL, "
|
||||
+ ApiAutocryptPeerColumns.LAST_SEEN + " INTEGER, "
|
||||
+ ApiAutocryptPeerColumns.LAST_SEEN_KEY + " INTEGER NULL, "
|
||||
+ ApiAutocryptPeerColumns.IS_MUTUAL + " INTEGER NULL, "
|
||||
+ ApiAutocryptPeerColumns.MASTER_KEY_ID + " INTEGER NULL, "
|
||||
+ ApiAutocryptPeerColumns.GOSSIP_MASTER_KEY_ID + " INTEGER NULL, "
|
||||
+ ApiAutocryptPeerColumns.GOSSIP_LAST_SEEN_KEY + " INTEGER NULL, "
|
||||
+ ApiAutocryptPeerColumns.GOSSIP_ORIGIN + " INTEGER NULL, "
|
||||
+ "PRIMARY KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ", "
|
||||
+ ApiAutocryptPeerColumns.IDENTIFIER + "), "
|
||||
+ "FOREIGN KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ") REFERENCES "
|
||||
+ Tables.API_APPS + "(" + ApiAppsColumns.PACKAGE_NAME + ") 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 NOT NULL UNIQUE, "
|
||||
+ ApiAppsColumns.PACKAGE_CERTIFICATE + " BLOB"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_API_APPS_ALLOWED_KEYS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.API_ALLOWED_KEYS + " ("
|
||||
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ ApiAppsAllowedKeysColumns.KEY_ID + " INTEGER, "
|
||||
+ ApiAppsAllowedKeysColumns.PACKAGE_NAME + " TEXT NOT NULL, "
|
||||
|
||||
+ "UNIQUE(" + ApiAppsAllowedKeysColumns.KEY_ID + ", "
|
||||
+ ApiAppsAllowedKeysColumns.PACKAGE_NAME + "), "
|
||||
+ "FOREIGN KEY(" + ApiAppsAllowedKeysColumns.PACKAGE_NAME + ") REFERENCES "
|
||||
+ Tables.API_APPS + "(" + ApiAppsAllowedKeysColumns.PACKAGE_NAME + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_OVERRIDDEN_WARNINGS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.OVERRIDDEN_WARNINGS + " ("
|
||||
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ OverriddenWarnings.IDENTIFIER + " TEXT NOT NULL UNIQUE "
|
||||
+ ")";
|
||||
|
||||
public KeychainDatabase(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
mContext = context;
|
||||
@Override
|
||||
public void onOpen(SupportSQLiteDatabase db) {
|
||||
super.onOpen(db);
|
||||
if (!db.isReadOnly()) {
|
||||
// Enable foreign key constraints
|
||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
||||
if (Constants.DEBUG) {
|
||||
recreateUnifiedKeyView(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
public SupportSQLiteDatabase getReadableDatabase() {
|
||||
return supportSQLiteOpenHelper.getReadableDatabase();
|
||||
}
|
||||
|
||||
public SupportSQLiteDatabase getWritableDatabase() {
|
||||
return supportSQLiteOpenHelper.getWritableDatabase();
|
||||
}
|
||||
|
||||
private void onCreate(SupportSQLiteDatabase db, Context context) {
|
||||
Timber.w("Creating database...");
|
||||
|
||||
db.execSQL(CREATE_KEYRINGS_PUBLIC);
|
||||
db.execSQL(CREATE_KEYRINGS_SECRET);
|
||||
db.execSQL(CREATE_KEYS);
|
||||
db.execSQL(CREATE_USER_PACKETS);
|
||||
db.execSQL(CREATE_CERTS);
|
||||
db.execSQL(CREATE_UPDATE_KEYS);
|
||||
db.execSQL(CREATE_KEY_SIGNATURES);
|
||||
db.execSQL(CREATE_API_APPS);
|
||||
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
|
||||
db.execSQL(CREATE_OVERRIDDEN_WARNINGS);
|
||||
db.execSQL(CREATE_API_AUTOCRYPT_PEERS);
|
||||
db.execSQL(KeyRingsPublicModel.CREATE_TABLE);
|
||||
db.execSQL(KeysModel.CREATE_TABLE);
|
||||
db.execSQL(UserPacketsModel.CREATE_TABLE);
|
||||
db.execSQL(CertsModel.CREATE_TABLE);
|
||||
db.execSQL(KeyMetadataModel.CREATE_TABLE);
|
||||
db.execSQL(KeySignaturesModel.CREATE_TABLE);
|
||||
db.execSQL(ApiAppsModel.CREATE_TABLE);
|
||||
db.execSQL(OverriddenWarningsModel.CREATE_TABLE);
|
||||
db.execSQL(AutocryptPeersModel.CREATE_TABLE);
|
||||
db.execSQL(ApiAllowedKeysModel.CREATE_TABLE);
|
||||
db.execSQL(KeysModel.UNIFIEDKEYVIEW);
|
||||
|
||||
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ", " + KeysColumns.MASTER_KEY_ID + ");");
|
||||
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
|
||||
@@ -241,20 +139,10 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
db.execSQL("CREATE INDEX uids_by_email ON user_packets ("
|
||||
+ UserPacketsColumns.EMAIL + ");");
|
||||
|
||||
Preferences.getPreferences(mContext).setKeySignaturesTableInitialized();
|
||||
Preferences.getPreferences(context).setKeySignaturesTableInitialized();
|
||||
}
|
||||
|
||||
@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) {
|
||||
private void onUpgrade(SupportSQLiteDatabase db, Context context, int oldVersion, int newVersion) {
|
||||
Timber.d("Upgrading db from " + oldVersion + " to " + newVersion);
|
||||
|
||||
switch (oldVersion) {
|
||||
@@ -293,7 +181,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
case 7:
|
||||
// new table for allowed key ids in API
|
||||
try {
|
||||
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
|
||||
db.execSQL(ApiAppsModel.CREATE_TABLE);
|
||||
} catch (Exception e) {
|
||||
// never mind, the column probably already existed
|
||||
}
|
||||
@@ -302,37 +190,37 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
db.execSQL("DROP TABLE IF EXISTS certs");
|
||||
db.execSQL("DROP TABLE IF EXISTS user_ids");
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS user_packets("
|
||||
+ "master_key_id INTEGER, "
|
||||
+ "type INT, "
|
||||
+ "user_id TEXT, "
|
||||
+ "attribute_data BLOB, "
|
||||
+ "master_key_id INTEGER, "
|
||||
+ "type INT, "
|
||||
+ "user_id TEXT, "
|
||||
+ "attribute_data BLOB, "
|
||||
|
||||
+ "is_primary INTEGER, "
|
||||
+ "is_revoked INTEGER, "
|
||||
+ "rank INTEGER, "
|
||||
+ "is_primary INTEGER, "
|
||||
+ "is_revoked INTEGER, "
|
||||
+ "rank INTEGER, "
|
||||
|
||||
+ "PRIMARY KEY(master_key_id, rank), "
|
||||
+ "FOREIGN KEY(master_key_id) REFERENCES "
|
||||
+ "PRIMARY KEY(master_key_id, rank), "
|
||||
+ "FOREIGN KEY(master_key_id) REFERENCES "
|
||||
+ "keyrings_public(master_key_id) ON DELETE CASCADE"
|
||||
+ ")");
|
||||
+ ")");
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS certs("
|
||||
+ "master_key_id INTEGER,"
|
||||
+ "rank INTEGER, " // rank of certified uid
|
||||
+ "master_key_id INTEGER,"
|
||||
+ "rank INTEGER, " // rank of certified uid
|
||||
|
||||
+ "key_id_certifier INTEGER, " // certifying key
|
||||
+ "type INTEGER, "
|
||||
+ "verified INTEGER, "
|
||||
+ "creation INTEGER, "
|
||||
+ "key_id_certifier INTEGER, " // certifying key
|
||||
+ "type INTEGER, "
|
||||
+ "verified INTEGER, "
|
||||
+ "creation INTEGER, "
|
||||
|
||||
+ "data BLOB, "
|
||||
+ "data BLOB, "
|
||||
|
||||
+ "PRIMARY KEY(master_key_id, rank, "
|
||||
+ "PRIMARY KEY(master_key_id, rank, "
|
||||
+ "key_id_certifier), "
|
||||
+ "FOREIGN KEY(master_key_id) REFERENCES "
|
||||
+ "FOREIGN KEY(master_key_id) REFERENCES "
|
||||
+ "keyrings_public(master_key_id) ON DELETE CASCADE,"
|
||||
+ "FOREIGN KEY(master_key_id, rank) REFERENCES "
|
||||
+ "FOREIGN KEY(master_key_id, rank) REFERENCES "
|
||||
+ "user_packets(master_key_id, rank) ON DELETE CASCADE"
|
||||
+ ")");
|
||||
+ ")");
|
||||
case 9:
|
||||
// do nothing here, just consolidate
|
||||
case 10:
|
||||
@@ -371,20 +259,12 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
case 19:
|
||||
// emergency fix for crashing consolidate
|
||||
db.execSQL("UPDATE keys SET is_secure = 1;");
|
||||
/* TODO actually drop this table. leaving it around for now!
|
||||
case 20:
|
||||
db.execSQL("DROP TABLE api_accounts");
|
||||
if (oldVersion == 20) {
|
||||
// no need to consolidate
|
||||
return;
|
||||
}
|
||||
*/
|
||||
case 20:
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS overridden_warnings ("
|
||||
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ "identifier TEXT NOT NULL UNIQUE "
|
||||
+ ")");
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS overridden_warnings ("
|
||||
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ "identifier TEXT NOT NULL UNIQUE "
|
||||
+ ")");
|
||||
|
||||
case 21:
|
||||
try {
|
||||
@@ -400,10 +280,10 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
+ "last_updated INTEGER NOT NULL, "
|
||||
+ "last_seen_key INTEGER NOT NULL, "
|
||||
+ "state INTEGER NOT NULL, "
|
||||
+ "master_key_id INTEGER NULL, "
|
||||
+ "master_key_id INTEGER, "
|
||||
+ "PRIMARY KEY(package_name, identifier), "
|
||||
+ "FOREIGN KEY(package_name) REFERENCES api_apps(package_name) ON DELETE CASCADE"
|
||||
+ ")");
|
||||
+ ")");
|
||||
|
||||
case 23:
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS key_signatures ("
|
||||
@@ -413,7 +293,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
+ "FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE"
|
||||
+ ")");
|
||||
|
||||
case 24:
|
||||
case 24: {
|
||||
try {
|
||||
db.beginTransaction();
|
||||
db.execSQL("ALTER TABLE api_autocrypt_peers RENAME TO tmp");
|
||||
@@ -456,11 +336,74 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS uids_by_email ON user_packets (email);");
|
||||
db.execSQL("DROP INDEX keys_by_rank");
|
||||
db.execSQL("CREATE INDEX keys_by_rank ON keys(rank, master_key_id);");
|
||||
}
|
||||
|
||||
case 25: {
|
||||
try {
|
||||
migrateSecretKeysFromDbToLocalStorage(db, context);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Error migrating secret keys! This is bad!!");
|
||||
}
|
||||
}
|
||||
|
||||
case 26:
|
||||
migrateUpdatedKeysToKeyMetadataTable(db);
|
||||
|
||||
case 27:
|
||||
renameApiAutocryptPeersTable(db);
|
||||
|
||||
case 28:
|
||||
recreateUnifiedKeyView(db);
|
||||
// drop old table from version 20
|
||||
db.execSQL("DROP TABLE IF EXISTS api_accounts");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
private void recreateUnifiedKeyView(SupportSQLiteDatabase db) {
|
||||
// noinspection deprecation
|
||||
db.execSQL("DROP VIEW IF EXISTS " + KeysModel.UNIFIEDKEYVIEW_VIEW_NAME);
|
||||
db.execSQL(KeysModel.UNIFIEDKEYVIEW);
|
||||
}
|
||||
|
||||
private void migrateSecretKeysFromDbToLocalStorage(SupportSQLiteDatabase db, Context context) throws IOException {
|
||||
LocalSecretKeyStorage localSecretKeyStorage = LocalSecretKeyStorage.getInstance(context);
|
||||
Cursor cursor = db.query("SELECT master_key_id, key_ring_data FROM keyrings_secret");
|
||||
while (cursor.moveToNext()) {
|
||||
long masterKeyId = cursor.getLong(0);
|
||||
byte[] secretKeyBlob = cursor.getBlob(1);
|
||||
localSecretKeyStorage.writeSecretKey(masterKeyId, secretKeyBlob);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// we'll keep this around for now, but make sure to delete when migration looks ok!!
|
||||
// db.execSQL("DROP TABLE keyrings_secret");
|
||||
}
|
||||
|
||||
private void migrateUpdatedKeysToKeyMetadataTable(SupportSQLiteDatabase db) {
|
||||
try {
|
||||
db.execSQL("ALTER TABLE updated_keys RENAME TO key_metadata;");
|
||||
} catch (SQLException e) {
|
||||
if (Constants.DEBUG) {
|
||||
Timber.e(e, "Ignoring migration exception, this probably happened before");
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void renameApiAutocryptPeersTable(SupportSQLiteDatabase db) {
|
||||
try {
|
||||
db.execSQL("ALTER TABLE api_autocrypt_peers RENAME TO autocrypt_peers;");
|
||||
} catch (SQLException e) {
|
||||
if (Constants.DEBUG) {
|
||||
Timber.e(e, "Ignoring migration exception, this probably happened before");
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void onDowngrade() {
|
||||
// Downgrade is ok for the debug version, makes it easier to work with branches
|
||||
if (Constants.DEBUG) {
|
||||
return;
|
||||
@@ -512,9 +455,9 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
|
||||
// DANGEROUS, use in test code ONLY!
|
||||
public void clearDatabase() {
|
||||
getWritableDatabase().execSQL("delete from " + Tables.KEY_RINGS_PUBLIC);
|
||||
getWritableDatabase().execSQL("delete from " + Tables.API_ALLOWED_KEYS);
|
||||
getWritableDatabase().execSQL("delete from " + Tables.API_APPS);
|
||||
getWritableDatabase().execSQL("delete from " + KeyRingsPublicModel.TABLE_NAME);
|
||||
getWritableDatabase().execSQL("delete from " + ApiAllowedKeysModel.TABLE_NAME);
|
||||
getWritableDatabase().execSQL("delete from api_apps");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.sufficientlysecure.keychain.daos;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.arch.persistence.db.SupportSQLiteDatabase;
|
||||
import android.arch.persistence.db.SupportSQLiteQuery;
|
||||
import android.database.Cursor;
|
||||
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
|
||||
|
||||
class AbstractDao {
|
||||
private final KeychainDatabase db;
|
||||
private final DatabaseNotifyManager databaseNotifyManager;
|
||||
|
||||
AbstractDao(KeychainDatabase db, DatabaseNotifyManager databaseNotifyManager) {
|
||||
this.db = db;
|
||||
this.databaseNotifyManager = databaseNotifyManager;
|
||||
}
|
||||
|
||||
SupportSQLiteDatabase getReadableDb() {
|
||||
return db.getReadableDatabase();
|
||||
}
|
||||
|
||||
SupportSQLiteDatabase getWritableDb() {
|
||||
return db.getWritableDatabase();
|
||||
}
|
||||
|
||||
DatabaseNotifyManager getDatabaseNotifyManager() {
|
||||
return databaseNotifyManager;
|
||||
}
|
||||
|
||||
<T> List<T> mapAllRows(SupportSQLiteQuery query, Mapper<T> mapper) {
|
||||
ArrayList<T> result = new ArrayList<>();
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
while (cursor.moveToNext()) {
|
||||
T item = mapper.map(cursor);
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
<T> T mapSingleRowOrThrow(SupportSQLiteQuery query, Mapper<T> mapper) throws NotFoundException {
|
||||
T result = mapSingleRow(query, mapper);
|
||||
if (result == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
<T> T mapSingleRow(SupportSQLiteQuery query, Mapper<T> mapper) {
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToNext()) {
|
||||
return mapper.map(cursor);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface Mapper<T> {
|
||||
T map(Cursor cursor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.daos;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.squareup.sqldelight.SqlDelightQuery;
|
||||
import org.sufficientlysecure.keychain.ApiAllowedKeysModel.InsertAllowedKey;
|
||||
import org.sufficientlysecure.keychain.ApiAppsModel;
|
||||
import org.sufficientlysecure.keychain.ApiAppsModel.DeleteByPackageName;
|
||||
import org.sufficientlysecure.keychain.ApiAppsModel.InsertApiApp;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.model.ApiAllowedKey;
|
||||
import org.sufficientlysecure.keychain.model.ApiApp;
|
||||
|
||||
|
||||
public class ApiAppDao extends AbstractDao {
|
||||
public static ApiAppDao getInstance(Context context) {
|
||||
KeychainDatabase keychainDatabase = KeychainDatabase.getInstance(context);
|
||||
DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
|
||||
return new ApiAppDao(keychainDatabase, databaseNotifyManager);
|
||||
}
|
||||
|
||||
private ApiAppDao(KeychainDatabase keychainDatabase, DatabaseNotifyManager databaseNotifyManager) {
|
||||
super(keychainDatabase, databaseNotifyManager);
|
||||
}
|
||||
|
||||
public ApiApp getApiApp(String packageName) {
|
||||
try (Cursor cursor = getReadableDb().query(ApiApp.FACTORY.selectByPackageName(packageName))) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return ApiApp.FACTORY.selectByPackageNameMapper().map(cursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getApiAppCertificate(String packageName) {
|
||||
try (Cursor cursor = getReadableDb().query(ApiApp.FACTORY.getCertificate(packageName))) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return ApiApp.FACTORY.getCertificateMapper().map(cursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void insertApiApp(ApiApp apiApp) {
|
||||
ApiApp existingApiApp = getApiApp(apiApp.package_name());
|
||||
if (existingApiApp != null) {
|
||||
if (!Arrays.equals(existingApiApp.package_signature(), apiApp.package_signature())) {
|
||||
throw new IllegalStateException("Inserting existing api with different signature?!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
InsertApiApp statement = new ApiAppsModel.InsertApiApp(getWritableDb());
|
||||
statement.bind(apiApp.package_name(), apiApp.package_signature());
|
||||
statement.executeInsert();
|
||||
|
||||
getDatabaseNotifyManager().notifyApiAppChange(apiApp.package_name());
|
||||
}
|
||||
|
||||
public void deleteApiApp(String packageName) {
|
||||
DeleteByPackageName deleteByPackageName = new DeleteByPackageName(getWritableDb());
|
||||
deleteByPackageName.bind(packageName);
|
||||
deleteByPackageName.executeUpdateDelete();
|
||||
|
||||
getDatabaseNotifyManager().notifyApiAppChange(packageName);
|
||||
}
|
||||
|
||||
public HashSet<Long> getAllowedKeyIdsForApp(String packageName) {
|
||||
SqlDelightQuery allowedKeys = ApiAllowedKey.FACTORY.getAllowedKeys(packageName);
|
||||
HashSet<Long> keyIds = new HashSet<>();
|
||||
try (Cursor cursor = getReadableDb().query(allowedKeys)) {
|
||||
while (cursor.moveToNext()) {
|
||||
long allowedKeyId = ApiAllowedKey.FACTORY.getAllowedKeysMapper().map(cursor);
|
||||
keyIds.add(allowedKeyId);
|
||||
}
|
||||
}
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public void saveAllowedKeyIdsForApp(String packageName, Set<Long> allowedKeyIds) {
|
||||
ApiAllowedKey.DeleteByPackageName deleteByPackageName = new ApiAllowedKey.DeleteByPackageName(getWritableDb());
|
||||
deleteByPackageName.bind(packageName);
|
||||
deleteByPackageName.executeUpdateDelete();
|
||||
|
||||
InsertAllowedKey statement = new InsertAllowedKey(getWritableDb());
|
||||
for (Long keyId : allowedKeyIds) {
|
||||
statement.bind(packageName, keyId);
|
||||
statement.execute();
|
||||
}
|
||||
|
||||
getDatabaseNotifyManager().notifyApiAppChange(packageName);
|
||||
}
|
||||
|
||||
public void addAllowedKeyIdForApp(String packageName, long allowedKeyId) {
|
||||
InsertAllowedKey statement = new InsertAllowedKey(getWritableDb());
|
||||
statement.bind(packageName, allowedKeyId);
|
||||
statement.execute();
|
||||
|
||||
getDatabaseNotifyManager().notifyApiAppChange(packageName);
|
||||
}
|
||||
|
||||
public List<ApiApp> getAllApiApps() {
|
||||
SqlDelightQuery query = ApiApp.FACTORY.selectAll();
|
||||
|
||||
ArrayList<ApiApp> result = new ArrayList<>();
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
while (cursor.moveToNext()) {
|
||||
ApiApp apiApp = ApiApp.FACTORY.selectAllMapper().map(cursor);
|
||||
result.add(apiApp);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.daos;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.squareup.sqldelight.SqlDelightQuery;
|
||||
import org.sufficientlysecure.keychain.AutocryptPeersModel.DeleteByIdentifier;
|
||||
import org.sufficientlysecure.keychain.AutocryptPeersModel.DeleteByMasterKeyId;
|
||||
import org.sufficientlysecure.keychain.AutocryptPeersModel.InsertPeer;
|
||||
import org.sufficientlysecure.keychain.AutocryptPeersModel.UpdateGossipKey;
|
||||
import org.sufficientlysecure.keychain.AutocryptPeersModel.UpdateKey;
|
||||
import org.sufficientlysecure.keychain.AutocryptPeersModel.UpdateLastSeen;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer.AutocryptKeyStatus;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin;
|
||||
|
||||
|
||||
public class AutocryptPeerDao extends AbstractDao {
|
||||
public static AutocryptPeerDao getInstance(Context context) {
|
||||
KeychainDatabase keychainDatabase = KeychainDatabase.getInstance(context);
|
||||
DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
|
||||
return new AutocryptPeerDao(keychainDatabase, databaseNotifyManager);
|
||||
}
|
||||
|
||||
private AutocryptPeerDao(KeychainDatabase database, DatabaseNotifyManager databaseNotifyManager) {
|
||||
super(database, databaseNotifyManager);
|
||||
}
|
||||
|
||||
public Long getMasterKeyIdForAutocryptPeer(String autocryptId) {
|
||||
SqlDelightQuery query = AutocryptPeer.FACTORY.selectMasterKeyIdByIdentifier(autocryptId);
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return AutocryptPeer.FACTORY.selectMasterKeyIdByIdentifierMapper().map(cursor);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AutocryptPeer getAutocryptPeer(String packageName, String autocryptId) {
|
||||
List<AutocryptPeer> autocryptPeers = getAutocryptPeers(packageName, autocryptId);
|
||||
if (!autocryptPeers.isEmpty()) {
|
||||
return autocryptPeers.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<AutocryptPeer> getAutocryptPeers(String packageName, String... autocryptId) {
|
||||
ArrayList<AutocryptPeer> result = new ArrayList<>(autocryptId.length);
|
||||
SqlDelightQuery query = AutocryptPeer.FACTORY.selectByIdentifiers(packageName, autocryptId);
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToNext()) {
|
||||
AutocryptPeer autocryptPeer = AutocryptPeer.PEER_MAPPER.map(cursor);
|
||||
result.add(autocryptPeer);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<AutocryptKeyStatus> getAutocryptKeyStatus(String packageName, String[] autocryptIds) {
|
||||
ArrayList<AutocryptKeyStatus> result = new ArrayList<>(autocryptIds.length);
|
||||
SqlDelightQuery query = AutocryptPeer.FACTORY.selectAutocryptKeyStatus(packageName, autocryptIds, System.currentTimeMillis());
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToNext()) {
|
||||
AutocryptKeyStatus autocryptPeer = AutocryptPeer.KEY_STATUS_MAPPER.map(cursor);
|
||||
result.add(autocryptPeer);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ensureAutocryptPeerExists(String packageName, String autocryptId) {
|
||||
InsertPeer insertStatement = new InsertPeer(getWritableDb());
|
||||
insertStatement.bind(packageName, autocryptId);
|
||||
insertStatement.executeInsert();
|
||||
}
|
||||
|
||||
public void insertOrUpdateLastSeen(String packageName, String autocryptId, Date date) {
|
||||
ensureAutocryptPeerExists(packageName, autocryptId);
|
||||
|
||||
UpdateLastSeen updateStatement = new UpdateLastSeen(getWritableDb(), AutocryptPeer.FACTORY);
|
||||
updateStatement.bind(packageName, autocryptId, date);
|
||||
updateStatement.executeUpdateDelete();
|
||||
}
|
||||
|
||||
public void updateKey(String packageName, String autocryptId, Date effectiveDate, long masterKeyId,
|
||||
boolean isMutual) {
|
||||
ensureAutocryptPeerExists(packageName, autocryptId);
|
||||
|
||||
UpdateKey updateStatement = new UpdateKey(getWritableDb(), AutocryptPeer.FACTORY);
|
||||
updateStatement.bind(packageName, autocryptId, effectiveDate, masterKeyId, isMutual);
|
||||
updateStatement.executeUpdateDelete();
|
||||
|
||||
getDatabaseNotifyManager().notifyAutocryptUpdate(autocryptId, masterKeyId);
|
||||
}
|
||||
|
||||
public void updateKeyGossip(String packageName, String autocryptId, Date effectiveDate, long masterKeyId,
|
||||
GossipOrigin origin) {
|
||||
ensureAutocryptPeerExists(packageName, autocryptId);
|
||||
|
||||
UpdateGossipKey updateStatement = new UpdateGossipKey(getWritableDb(), AutocryptPeer.FACTORY);
|
||||
updateStatement.bind(packageName, autocryptId, effectiveDate, masterKeyId, origin);
|
||||
updateStatement.executeUpdateDelete();
|
||||
|
||||
getDatabaseNotifyManager().notifyAutocryptUpdate(autocryptId, masterKeyId);
|
||||
}
|
||||
|
||||
public List<AutocryptPeer> getAutocryptPeersForKey(long masterKeyId) {
|
||||
ArrayList<AutocryptPeer> result = new ArrayList<>();
|
||||
SqlDelightQuery query = AutocryptPeer.FACTORY.selectByMasterKeyId(masterKeyId);
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToNext()) {
|
||||
AutocryptPeer autocryptPeer = AutocryptPeer.PEER_MAPPER.map(cursor);
|
||||
result.add(autocryptPeer);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void deleteByIdentifier(String packageName, String autocryptId) {
|
||||
Long masterKeyId = getMasterKeyIdForAutocryptPeer(autocryptId);
|
||||
DeleteByIdentifier deleteStatement = new DeleteByIdentifier(getReadableDb());
|
||||
deleteStatement.bind(packageName, autocryptId);
|
||||
deleteStatement.execute();
|
||||
if (masterKeyId != null) {
|
||||
getDatabaseNotifyManager().notifyAutocryptDelete(autocryptId, masterKeyId);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteByMasterKeyId(long masterKeyId) {
|
||||
DeleteByMasterKeyId deleteStatement = new DeleteByMasterKeyId(getReadableDb());
|
||||
deleteStatement.bind(masterKeyId);
|
||||
deleteStatement.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.sufficientlysecure.keychain.daos;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.squareup.sqldelight.SqlDelightQuery;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.model.Certification;
|
||||
import org.sufficientlysecure.keychain.model.Certification.CertDetails;
|
||||
|
||||
|
||||
public class CertificationDao extends AbstractDao {
|
||||
public static CertificationDao getInstance(Context context) {
|
||||
KeychainDatabase keychainDatabase = KeychainDatabase.getInstance(context);
|
||||
DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
|
||||
return new CertificationDao(keychainDatabase, databaseNotifyManager);
|
||||
}
|
||||
|
||||
private CertificationDao(KeychainDatabase keychainDatabase, DatabaseNotifyManager databaseNotifyManager) {
|
||||
super(keychainDatabase, databaseNotifyManager);
|
||||
}
|
||||
|
||||
public CertDetails getVerifyingCertDetails(long masterKeyId, int userPacketRank) {
|
||||
SqlDelightQuery query = Certification.FACTORY.selectVerifyingCertDetails(masterKeyId, userPacketRank);
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return Certification.CERT_DETAILS_MAPPER.map(cursor);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.sufficientlysecure.keychain.daos;
|
||||
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
|
||||
public class DatabaseNotifyManager {
|
||||
private static final Uri URI_KEYS = Uri.parse("content://" + Constants.PROVIDER_AUTHORITY + "/keys");
|
||||
private static final Uri URI_APPS = Uri.parse("content://" + Constants.PROVIDER_AUTHORITY + "/apps");
|
||||
|
||||
private ContentResolver contentResolver;
|
||||
|
||||
public static DatabaseNotifyManager create(Context context) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
return new DatabaseNotifyManager(contentResolver);
|
||||
}
|
||||
|
||||
private DatabaseNotifyManager(ContentResolver contentResolver) {
|
||||
this.contentResolver = contentResolver;
|
||||
}
|
||||
|
||||
public void notifyKeyChange(long masterKeyId) {
|
||||
Uri uri = getNotifyUriMasterKeyId(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public void notifyAutocryptDelete(String autocryptId, Long masterKeyId) {
|
||||
Uri uri = getNotifyUriMasterKeyId(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public void notifyAutocryptUpdate(String autocryptId, long masterKeyId) {
|
||||
Uri uri = getNotifyUriMasterKeyId(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public void notifyKeyMetadataChange(long masterKeyId) {
|
||||
Uri uri = getNotifyUriMasterKeyId(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public void notifyApiAppChange(String apiApp) {
|
||||
Uri uri = getNotifyUriPackageName(apiApp);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public static Uri getNotifyUriAllKeys() {
|
||||
return URI_KEYS;
|
||||
}
|
||||
|
||||
public static Uri getNotifyUriMasterKeyId(long masterKeyId) {
|
||||
return URI_KEYS.buildUpon().appendPath(Long.toString(masterKeyId)).build();
|
||||
}
|
||||
|
||||
public static Uri getNotifyUriAllApps() {
|
||||
return URI_APPS;
|
||||
}
|
||||
|
||||
public static Uri getNotifyUriPackageName(String packageName) {
|
||||
return URI_APPS.buildUpon().appendPath(packageName).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.sufficientlysecure.keychain.daos;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.squareup.sqldelight.SqlDelightQuery;
|
||||
import org.sufficientlysecure.keychain.KeyMetadataModel.ReplaceKeyMetadata;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.model.KeyMetadata;
|
||||
|
||||
|
||||
public class KeyMetadataDao extends AbstractDao {
|
||||
public static KeyMetadataDao create(Context context) {
|
||||
KeychainDatabase database = KeychainDatabase.getInstance(context);
|
||||
DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
|
||||
return new KeyMetadataDao(database, databaseNotifyManager);
|
||||
}
|
||||
|
||||
private KeyMetadataDao(KeychainDatabase database, DatabaseNotifyManager databaseNotifyManager) {
|
||||
super(database, databaseNotifyManager);
|
||||
}
|
||||
|
||||
public KeyMetadata getKeyMetadata(long masterKeyId) {
|
||||
SqlDelightQuery query = KeyMetadata.FACTORY.selectByMasterKeyId(masterKeyId);
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return KeyMetadata.FACTORY.selectByMasterKeyIdMapper().map(cursor);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void resetAllLastUpdatedTimes() {
|
||||
new KeyMetadata.DeleteAllLastUpdatedTimes(getWritableDb()).execute();
|
||||
}
|
||||
|
||||
public void renewKeyLastUpdatedTime(long masterKeyId, boolean seenOnKeyservers) {
|
||||
ReplaceKeyMetadata replaceStatement = new ReplaceKeyMetadata(getWritableDb(), KeyMetadata.FACTORY);
|
||||
replaceStatement.bind(masterKeyId, new Date(), seenOnKeyservers);
|
||||
replaceStatement.executeInsert();
|
||||
|
||||
getDatabaseNotifyManager().notifyKeyMetadataChange(masterKeyId);
|
||||
}
|
||||
|
||||
public List<byte[]> getFingerprintsForKeysOlderThan(long olderThan, TimeUnit timeUnit) {
|
||||
SqlDelightQuery query = KeyMetadata.FACTORY.selectFingerprintsForKeysOlderThan(new Date(timeUnit.toMillis(olderThan)));
|
||||
|
||||
List<byte[]> fingerprintList = new ArrayList<>();
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
while (cursor.moveToNext()) {
|
||||
byte[] fingerprint = KeyMetadata.FACTORY.selectFingerprintsForKeysOlderThanMapper().map(cursor);
|
||||
fingerprintList.add(fingerprint);
|
||||
}
|
||||
}
|
||||
return fingerprintList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.daos;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.WorkerThread;
|
||||
|
||||
import com.squareup.sqldelight.SqlDelightQuery;
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.model.Certification;
|
||||
import org.sufficientlysecure.keychain.model.KeyRingPublic;
|
||||
import org.sufficientlysecure.keychain.model.KeySignature;
|
||||
import org.sufficientlysecure.keychain.model.SubKey;
|
||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
||||
import org.sufficientlysecure.keychain.model.UserPacket;
|
||||
import org.sufficientlysecure.keychain.model.UserPacket.UserId;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
@WorkerThread
|
||||
public class KeyRepository extends AbstractDao {
|
||||
final ContentResolver contentResolver;
|
||||
final LocalPublicKeyStorage mLocalPublicKeyStorage;
|
||||
final LocalSecretKeyStorage localSecretKeyStorage;
|
||||
|
||||
OperationLog mLog;
|
||||
int mIndent;
|
||||
|
||||
public static KeyRepository create(Context context) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
LocalPublicKeyStorage localPublicKeyStorage = LocalPublicKeyStorage.getInstance(context);
|
||||
LocalSecretKeyStorage localSecretKeyStorage = LocalSecretKeyStorage.getInstance(context);
|
||||
KeychainDatabase database = KeychainDatabase.getInstance(context);
|
||||
DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
|
||||
return new KeyRepository(contentResolver, database, databaseNotifyManager, localPublicKeyStorage, localSecretKeyStorage);
|
||||
}
|
||||
|
||||
private KeyRepository(ContentResolver contentResolver, KeychainDatabase database,
|
||||
DatabaseNotifyManager databaseNotifyManager,
|
||||
LocalPublicKeyStorage localPublicKeyStorage,
|
||||
LocalSecretKeyStorage localSecretKeyStorage) {
|
||||
this(contentResolver, database, databaseNotifyManager, localPublicKeyStorage, localSecretKeyStorage, new OperationLog(), 0);
|
||||
}
|
||||
|
||||
KeyRepository(ContentResolver contentResolver, KeychainDatabase database,
|
||||
DatabaseNotifyManager databaseNotifyManager,
|
||||
LocalPublicKeyStorage localPublicKeyStorage,
|
||||
LocalSecretKeyStorage localSecretKeyStorage,
|
||||
OperationLog log, int indent) {
|
||||
super(database, databaseNotifyManager);
|
||||
this.contentResolver = contentResolver;
|
||||
mLocalPublicKeyStorage = localPublicKeyStorage;
|
||||
this.localSecretKeyStorage = localSecretKeyStorage;
|
||||
mIndent = indent;
|
||||
mLog = log;
|
||||
}
|
||||
|
||||
public OperationLog getLog() {
|
||||
return mLog;
|
||||
}
|
||||
|
||||
public void log(LogType type) {
|
||||
if (mLog != null) {
|
||||
mLog.add(type, mIndent);
|
||||
}
|
||||
}
|
||||
|
||||
public void log(LogType type, Object... parameters) {
|
||||
if (mLog != null) {
|
||||
mLog.add(type, mIndent, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearLog() {
|
||||
mLog = new OperationLog();
|
||||
}
|
||||
|
||||
public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(long masterKeyId) throws NotFoundException {
|
||||
UnifiedKeyInfo unifiedKeyInfo = getUnifiedKeyInfo(masterKeyId);
|
||||
if (unifiedKeyInfo == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
byte[] publicKeyData = loadPublicKeyRingData(masterKeyId);
|
||||
return new CanonicalizedPublicKeyRing(publicKeyData, unifiedKeyInfo.verified());
|
||||
}
|
||||
|
||||
public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(long masterKeyId) throws NotFoundException {
|
||||
UnifiedKeyInfo unifiedKeyInfo = getUnifiedKeyInfo(masterKeyId);
|
||||
if (unifiedKeyInfo == null || !unifiedKeyInfo.has_any_secret()) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
byte[] secretKeyData = loadSecretKeyRingData(masterKeyId);
|
||||
if (secretKeyData == null) {
|
||||
throw new IllegalStateException("Missing expected secret key data!");
|
||||
}
|
||||
return new CanonicalizedSecretKeyRing(secretKeyData, unifiedKeyInfo.verified());
|
||||
}
|
||||
|
||||
public List<Long> getAllMasterKeyIds() {
|
||||
SqlDelightQuery query = KeyRingPublic.FACTORY.selectAllMasterKeyIds();
|
||||
return mapAllRows(query, KeyRingPublic.FACTORY.selectAllMasterKeyIdsMapper()::map);
|
||||
}
|
||||
|
||||
public List<Long> getMasterKeyIdsBySigner(List<Long> signerMasterKeyIds) {
|
||||
long[] signerKeyIds = getLongListAsArray(signerMasterKeyIds);
|
||||
SqlDelightQuery query = KeySignature.FACTORY.selectMasterKeyIdsBySigner(signerKeyIds);
|
||||
return mapAllRows(query, KeySignature.FACTORY.selectMasterKeyIdsBySignerMapper()::map);
|
||||
}
|
||||
|
||||
public Long getMasterKeyIdBySubkeyId(long subKeyId) {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectMasterKeyIdBySubkey(subKeyId);
|
||||
return mapSingleRow(query, SubKey.FACTORY.selectMasterKeyIdBySubkeyMapper()::map);
|
||||
}
|
||||
|
||||
public UnifiedKeyInfo getUnifiedKeyInfo(long masterKeyId) {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectUnifiedKeyInfoByMasterKeyId(masterKeyId);
|
||||
return mapSingleRow(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map);
|
||||
}
|
||||
|
||||
public List<UnifiedKeyInfo> getUnifiedKeyInfo(long... masterKeyIds) {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectUnifiedKeyInfoByMasterKeyIds(masterKeyIds);
|
||||
return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map);
|
||||
}
|
||||
|
||||
public List<UnifiedKeyInfo> getUnifiedKeyInfosByMailAddress(String mailAddress) {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectUnifiedKeyInfoSearchMailAddress('%' + mailAddress + '%');
|
||||
return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map);
|
||||
}
|
||||
|
||||
public List<UnifiedKeyInfo> getAllUnifiedKeyInfo() {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectAllUnifiedKeyInfo();
|
||||
return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map);
|
||||
}
|
||||
|
||||
public List<UnifiedKeyInfo> getAllUnifiedKeyInfoWithSecret() {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectAllUnifiedKeyInfoWithSecret();
|
||||
return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map);
|
||||
}
|
||||
|
||||
public List<UserId> getUserIds(long... masterKeyIds) {
|
||||
SqlDelightQuery query = UserPacket.FACTORY.selectUserIdsByMasterKeyId(masterKeyIds);
|
||||
return mapAllRows(query, UserPacket.USER_ID_MAPPER::map);
|
||||
}
|
||||
|
||||
public List<String> getConfirmedUserIds(long masterKeyId) {
|
||||
SqlDelightQuery query = UserPacket.FACTORY.selectUserIdsByMasterKeyIdAndVerification(
|
||||
Certification.FACTORY, masterKeyId, VerificationStatus.VERIFIED_SECRET);
|
||||
return mapAllRows(query, (cursor) -> UserPacket.USER_ID_MAPPER.map(cursor).user_id());
|
||||
}
|
||||
|
||||
public List<SubKey> getSubKeysByMasterKeyId(long masterKeyId) {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectSubkeysByMasterKeyId(masterKeyId);
|
||||
return mapAllRows(query, SubKey.SUBKEY_MAPPER::map);
|
||||
}
|
||||
|
||||
public SecretKeyType getSecretKeyType(long keyId) throws NotFoundException {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectSecretKeyType(keyId);
|
||||
return mapSingleRowOrThrow(query, SubKey.SKT_MAPPER::map);
|
||||
}
|
||||
|
||||
public byte[] getFingerprintByKeyId(long keyId) throws NotFoundException {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectFingerprintByKeyId(keyId);
|
||||
return mapSingleRowOrThrow(query, SubKey.FACTORY.selectFingerprintByKeyIdMapper()::map);
|
||||
}
|
||||
|
||||
private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ArmoredOutputStream aos = new ArmoredOutputStream(bos);
|
||||
|
||||
aos.write(data);
|
||||
aos.close();
|
||||
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public String getPublicKeyRingAsArmoredString(long masterKeyId) throws NotFoundException, IOException {
|
||||
byte[] data = loadPublicKeyRingData(masterKeyId);
|
||||
byte[] armoredData = getKeyRingAsArmoredData(data);
|
||||
return new String(armoredData);
|
||||
}
|
||||
|
||||
public byte[] getSecretKeyRingAsArmoredData(long masterKeyId) throws NotFoundException, IOException {
|
||||
byte[] data = loadSecretKeyRingData(masterKeyId);
|
||||
return getKeyRingAsArmoredData(data);
|
||||
}
|
||||
|
||||
public ContentResolver getContentResolver() {
|
||||
return contentResolver;
|
||||
}
|
||||
|
||||
public final byte[] loadPublicKeyRingData(long masterKeyId) throws NotFoundException {
|
||||
SqlDelightQuery query = KeyRingPublic.FACTORY.selectByMasterKeyId(masterKeyId);
|
||||
try (Cursor cursor = getReadableDb().query(query)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
KeyRingPublic keyRingPublic = KeyRingPublic.MAPPER.map(cursor);
|
||||
byte[] keyRingData = keyRingPublic.key_ring_data();
|
||||
if (keyRingData == null) {
|
||||
keyRingData = mLocalPublicKeyStorage.readPublicKey(masterKeyId);
|
||||
}
|
||||
return keyRingData;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Error reading public key from storage!");
|
||||
}
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
public final byte[] loadSecretKeyRingData(long masterKeyId) throws NotFoundException {
|
||||
try {
|
||||
return localSecretKeyStorage.readSecretKey(masterKeyId);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Error reading secret key from storage!");
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
public long getSecretSignId(long masterKeyId) throws NotFoundException {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectEffectiveSignKeyIdByMasterKeyId(masterKeyId);
|
||||
return mapSingleRowOrThrow(query, SubKey.FACTORY.selectEffectiveSignKeyIdByMasterKeyIdMapper()::map);
|
||||
}
|
||||
|
||||
public long getSecretAuthenticationId(long masterKeyId) throws NotFoundException {
|
||||
SqlDelightQuery query = SubKey.FACTORY.selectEffectiveAuthKeyIdByMasterKeyId(masterKeyId);
|
||||
return mapSingleRowOrThrow(query, SubKey.FACTORY.selectEffectiveAuthKeyIdByMasterKeyIdMapper()::map);
|
||||
}
|
||||
|
||||
public static class NotFoundException extends Exception {
|
||||
public NotFoundException() {
|
||||
}
|
||||
|
||||
public NotFoundException(String name) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
|
||||
private long[] getLongListAsArray(List<Long> longList) {
|
||||
long[] longs = new long[longList.size()];
|
||||
int i = 0;
|
||||
for (Long aLong : longList) {
|
||||
longs[i++] = aLong;
|
||||
}
|
||||
return longs;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
package org.sufficientlysecure.keychain.daos;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -29,20 +29,23 @@ import android.content.ContentProviderOperation;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.support.v4.util.LongSparseArray;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
import org.sufficientlysecure.keychain.KeyRingsPublicModel.DeleteByMasterKeyId;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.model.CustomColumnAdapters;
|
||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.UpdateTrustResult;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
@@ -55,13 +58,11 @@ import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeySignatures;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
@@ -84,69 +85,56 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
private static final int MAX_CACHED_KEY_SIZE = 1024 * 50;
|
||||
|
||||
private final Context context;
|
||||
private final LastUpdateInteractor lastUpdateInteractor;
|
||||
private DatabaseNotifyManager databaseNotifyManager;
|
||||
private final DatabaseNotifyManager databaseNotifyManager;
|
||||
private AutocryptPeerDao autocryptPeerDao;
|
||||
|
||||
public static KeyWritableRepository create(Context context) {
|
||||
LocalPublicKeyStorage localPublicKeyStorage = LocalPublicKeyStorage.getInstance(context);
|
||||
LastUpdateInteractor lastUpdateInteractor = LastUpdateInteractor.create(context);
|
||||
LocalSecretKeyStorage localSecretKeyStorage = LocalSecretKeyStorage.getInstance(context);
|
||||
DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
AutocryptPeerDao autocryptPeerDao = AutocryptPeerDao.getInstance(context);
|
||||
KeychainDatabase database = KeychainDatabase.getInstance(context);
|
||||
|
||||
return new KeyWritableRepository(context, localPublicKeyStorage, lastUpdateInteractor,
|
||||
databaseNotifyManager);
|
||||
return new KeyWritableRepository(context, database,
|
||||
localPublicKeyStorage, localSecretKeyStorage, databaseNotifyManager, autocryptPeerDao);
|
||||
}
|
||||
|
||||
private KeyWritableRepository(Context context,
|
||||
KeychainDatabase database, LocalPublicKeyStorage localPublicKeyStorage,
|
||||
LocalSecretKeyStorage localSecretKeyStorage,
|
||||
DatabaseNotifyManager databaseNotifyManager, AutocryptPeerDao autocryptPeerDao) {
|
||||
this(context, database, localPublicKeyStorage, localSecretKeyStorage, databaseNotifyManager, new OperationLog(), 0,
|
||||
autocryptPeerDao);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
KeyWritableRepository(Context context, LocalPublicKeyStorage localPublicKeyStorage,
|
||||
LastUpdateInteractor lastUpdateInteractor, DatabaseNotifyManager databaseNotifyManager) {
|
||||
this(context, localPublicKeyStorage, lastUpdateInteractor, new OperationLog(), 0,
|
||||
databaseNotifyManager);
|
||||
}
|
||||
|
||||
private KeyWritableRepository(Context context, LocalPublicKeyStorage localPublicKeyStorage,
|
||||
LastUpdateInteractor lastUpdateInteractor, OperationLog log, int indent,
|
||||
DatabaseNotifyManager databaseNotifyManager) {
|
||||
super(context.getContentResolver(), localPublicKeyStorage, log, indent);
|
||||
private KeyWritableRepository(Context context, KeychainDatabase database,
|
||||
LocalPublicKeyStorage localPublicKeyStorage,
|
||||
LocalSecretKeyStorage localSecretKeyStorage, DatabaseNotifyManager databaseNotifyManager,
|
||||
OperationLog log, int indent, AutocryptPeerDao autocryptPeerDao) {
|
||||
super(context.getContentResolver(), database, databaseNotifyManager, localPublicKeyStorage, localSecretKeyStorage, log, indent);
|
||||
|
||||
this.context = context;
|
||||
this.databaseNotifyManager = databaseNotifyManager;
|
||||
this.lastUpdateInteractor = lastUpdateInteractor;
|
||||
this.autocryptPeerDao = autocryptPeerDao;
|
||||
}
|
||||
|
||||
private LongSparseArray<CanonicalizedPublicKey> getTrustedMasterKeys() {
|
||||
Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), new String[] {
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
// we pick from cache only information that is not easily available from keyrings
|
||||
KeyRings.HAS_ANY_SECRET, KeyRings.VERIFIED
|
||||
}, KeyRings.HAS_ANY_SECRET + " = 1", null, null);
|
||||
LongSparseArray<CanonicalizedPublicKey> result = new LongSparseArray<>();
|
||||
|
||||
try {
|
||||
LongSparseArray<CanonicalizedPublicKey> result = new LongSparseArray<>();
|
||||
|
||||
if (cursor == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
long masterKeyId = cursor.getLong(0);
|
||||
int verified = cursor.getInt(2);
|
||||
byte[] blob = loadPublicKeyRingData(masterKeyId);
|
||||
if (blob != null) {
|
||||
result.put(masterKeyId, new CanonicalizedPublicKeyRing(blob, verified).getPublicKey());
|
||||
}
|
||||
} catch (NotFoundException e) {
|
||||
throw new IllegalStateException("Error reading secret key data, this should not happen!", e);
|
||||
List<UnifiedKeyInfo> unifiedKeyInfoWithSecret = getAllUnifiedKeyInfoWithSecret();
|
||||
for (UnifiedKeyInfo unifiedKeyInfo : unifiedKeyInfoWithSecret) {
|
||||
try {
|
||||
byte[] blob = loadPublicKeyRingData(unifiedKeyInfo.master_key_id());
|
||||
if (blob != null) {
|
||||
result.put(unifiedKeyInfo.master_key_id(),
|
||||
new CanonicalizedPublicKeyRing(blob, unifiedKeyInfo.verified()).getPublicKey());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
} catch (NotFoundException e) {
|
||||
throw new IllegalStateException("Error reading secret key data, this should not happen!", e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// bits, in order: CESA. make SURE these are correct, we will get bad log entries otherwise!!
|
||||
@@ -322,15 +310,17 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
|
||||
}
|
||||
|
||||
// do we have a trusted key for this?
|
||||
if (trustedKeys.indexOfKey(certId) < 0) {
|
||||
if (!signerKeyIds.contains(certId)) {
|
||||
operations.add(ContentProviderOperation.newInsert(KeySignatures.CONTENT_URI)
|
||||
.withValue(KeySignatures.MASTER_KEY_ID, masterKeyId)
|
||||
.withValue(KeySignatures.SIGNER_KEY_ID, certId)
|
||||
.build());
|
||||
signerKeyIds.add(certId);
|
||||
}
|
||||
// keep a note about the issuer of this key signature
|
||||
if (!signerKeyIds.contains(certId)) {
|
||||
operations.add(ContentProviderOperation.newInsert(KeySignatures.CONTENT_URI)
|
||||
.withValue(KeySignatures.MASTER_KEY_ID, masterKeyId)
|
||||
.withValue(KeySignatures.SIGNER_KEY_ID, certId)
|
||||
.build());
|
||||
signerKeyIds.add(certId);
|
||||
}
|
||||
|
||||
boolean isSignatureFromTrustedKey = trustedKeys.indexOfKey(certId) >= 0;
|
||||
if (!isSignatureFromTrustedKey) {
|
||||
unknownCerts += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -495,7 +485,7 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
|
||||
if (item.selfRevocation != null) {
|
||||
operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfRevocation,
|
||||
Certs.VERIFIED_SELF));
|
||||
VerificationStatus.VERIFIED_SELF));
|
||||
// don't bother with trusted certs if the uid is revoked, anyways
|
||||
continue;
|
||||
}
|
||||
@@ -505,7 +495,7 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
}
|
||||
|
||||
operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfCert,
|
||||
selfCertsAreTrusted ? Certs.VERIFIED_SECRET : Certs.VERIFIED_SELF));
|
||||
selfCertsAreTrusted ? VerificationStatus.VERIFIED_SECRET : VerificationStatus.VERIFIED_SELF));
|
||||
|
||||
// iterate over signatures
|
||||
for (int i = 0; i < item.trustedCerts.size(); i++) {
|
||||
@@ -517,7 +507,7 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
}
|
||||
// otherwise, build database operation
|
||||
operations.add(buildCertOperations(
|
||||
masterKeyId, userIdRank, sig, Certs.VERIFIED_SECRET));
|
||||
masterKeyId, userIdRank, sig, VerificationStatus.VERIFIED_SECRET));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,16 +519,13 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
mIndent -= 1;
|
||||
}
|
||||
|
||||
ContentProviderOperation lastUpdateReinsertOp = getLastUpdatedReinsertOperationByMasterKeyId(masterKeyId);
|
||||
if (lastUpdateReinsertOp != null) {
|
||||
operations.add(lastUpdateReinsertOp);
|
||||
}
|
||||
|
||||
try {
|
||||
// delete old version of this keyRing (from database only!), which also deletes all keys and userIds on cascade
|
||||
int deleted = contentResolver.delete(
|
||||
KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null);
|
||||
if (deleted > 0) {
|
||||
DeleteByMasterKeyId deleteStatement = new DeleteByMasterKeyId(getWritableDb());
|
||||
deleteStatement.bind(masterKeyId);
|
||||
int deletedRows = deleteStatement.executeUpdateDelete();
|
||||
|
||||
if (deletedRows > 0) {
|
||||
log(LogType.MSG_IP_DELETE_OLD_OK);
|
||||
result |= SaveKeyringResult.UPDATED;
|
||||
} else {
|
||||
@@ -564,26 +551,6 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
|
||||
}
|
||||
|
||||
private ContentProviderOperation getLastUpdatedReinsertOperationByMasterKeyId(long masterKeyId) {
|
||||
Long lastUpdateTime = getLastUpdateTime(masterKeyId);
|
||||
if (lastUpdateTime == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Boolean seenOnKeyservers = lastUpdateInteractor.getSeenOnKeyservers(masterKeyId);
|
||||
|
||||
ContentValues lastUpdatedEntry = new ContentValues(2);
|
||||
lastUpdatedEntry.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId);
|
||||
lastUpdatedEntry.put(UpdatedKeys.LAST_UPDATED, lastUpdateTime);
|
||||
if (seenOnKeyservers != null){
|
||||
lastUpdatedEntry.put(UpdatedKeys.SEEN_ON_KEYSERVERS, seenOnKeyservers);
|
||||
}
|
||||
return ContentProviderOperation
|
||||
.newInsert(UpdatedKeys.CONTENT_URI)
|
||||
.withValues(lastUpdatedEntry)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void writePublicKeyRing(CanonicalizedPublicKeyRing keyRing, long masterKeyId,
|
||||
ArrayList<ContentProviderOperation> operations) throws IOException {
|
||||
byte[] encodedKey = keyRing.getEncoded();
|
||||
@@ -601,27 +568,24 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
operations.add(ContentProviderOperation.newInsert(uri).withValues(values).build());
|
||||
}
|
||||
|
||||
private Uri writeSecretKeyRing(CanonicalizedSecretKeyRing keyRing, long masterKeyId) throws IOException {
|
||||
private void writeSecretKeyRing(CanonicalizedSecretKeyRing keyRing, long masterKeyId) throws IOException {
|
||||
byte[] encodedKey = keyRing.getEncoded();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeyRingData.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(KeyRingData.KEY_RING_DATA, encodedKey);
|
||||
|
||||
// insert new version of this keyRing
|
||||
Uri uri = KeyRingData.buildSecretKeyRingUri(masterKeyId);
|
||||
return contentResolver.insert(uri, values);
|
||||
localSecretKeyStorage.writeSecretKey(masterKeyId, encodedKey);
|
||||
}
|
||||
|
||||
public boolean deleteKeyRing(long masterKeyId) {
|
||||
try {
|
||||
mLocalPublicKeyStorage.deletePublicKey(masterKeyId);
|
||||
localSecretKeyStorage.deleteSecretKey(masterKeyId);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Could not delete file!");
|
||||
return false;
|
||||
}
|
||||
contentResolver.delete(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),null, null);
|
||||
int deletedRows = contentResolver.delete(KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null);
|
||||
autocryptPeerDao.deleteByMasterKeyId(masterKeyId);
|
||||
|
||||
DeleteByMasterKeyId deleteStatement = new DeleteByMasterKeyId(getWritableDb());
|
||||
deleteStatement.bind(masterKeyId);
|
||||
int deletedRows = deleteStatement.executeUpdateDelete();
|
||||
|
||||
databaseNotifyManager.notifyKeyChange(masterKeyId);
|
||||
|
||||
@@ -683,11 +647,7 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
|
||||
// save secret keyring
|
||||
try {
|
||||
Uri insertedUri = writeSecretKeyRing(keyRing, masterKeyId);
|
||||
if (insertedUri == null) {
|
||||
log(LogType.MSG_IS_DB_EXCEPTION);
|
||||
return SaveKeyringResult.RESULT_ERROR;
|
||||
}
|
||||
writeSecretKeyRing(keyRing, masterKeyId);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Failed to encode key!");
|
||||
log(LogType.MSG_IS_ERROR_IO_EXC);
|
||||
@@ -1039,30 +999,18 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
|
||||
log.add(LogType.MSG_TRUST, 0);
|
||||
|
||||
Cursor cursor;
|
||||
Preferences preferences = Preferences.getPreferences(context);
|
||||
boolean isTrustDbInitialized = preferences.isKeySignaturesTableInitialized();
|
||||
|
||||
List<Long> masterKeyIds;
|
||||
if (!isTrustDbInitialized) {
|
||||
log.add(LogType.MSG_TRUST_INITIALIZE, 1);
|
||||
cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(),
|
||||
new String[] { KeyRings.MASTER_KEY_ID }, null, null, null);
|
||||
masterKeyIds = getAllMasterKeyIds();
|
||||
} else {
|
||||
String[] signerMasterKeyIdStrings = new String[signerMasterKeyIds.size()];
|
||||
int i = 0;
|
||||
for (Long masterKeyId : signerMasterKeyIds) {
|
||||
log.add(LogType.MSG_TRUST_KEY, 1, KeyFormattingUtils.beautifyKeyId(masterKeyId));
|
||||
signerMasterKeyIdStrings[i++] = Long.toString(masterKeyId);
|
||||
}
|
||||
|
||||
cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsFilterBySigner(),
|
||||
new String[] { KeyRings.MASTER_KEY_ID }, null, signerMasterKeyIdStrings, null);
|
||||
masterKeyIds = getMasterKeyIdsBySigner(signerMasterKeyIds);
|
||||
}
|
||||
|
||||
if (cursor == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
int totalKeys = cursor.getCount();
|
||||
int totalKeys = masterKeyIds.size();
|
||||
int processedKeys = 0;
|
||||
|
||||
if (totalKeys == 0) {
|
||||
@@ -1072,41 +1020,37 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
log.add(LogType.MSG_TRUST_COUNT, 1, totalKeys);
|
||||
}
|
||||
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
long masterKeyId = cursor.getLong(0);
|
||||
for (long masterKeyId : masterKeyIds) {
|
||||
try {
|
||||
log.add(LogType.MSG_TRUST_KEY, 1, KeyFormattingUtils.beautifyKeyId(masterKeyId));
|
||||
|
||||
byte[] pubKeyData = loadPublicKeyRingData(masterKeyId);
|
||||
UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(pubKeyData);
|
||||
byte[] pubKeyData = loadPublicKeyRingData(masterKeyId);
|
||||
UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(pubKeyData);
|
||||
|
||||
clearLog();
|
||||
SaveKeyringResult result = savePublicKeyRing(uncachedKeyRing, true);
|
||||
clearLog();
|
||||
SaveKeyringResult result = savePublicKeyRing(uncachedKeyRing, true);
|
||||
|
||||
log.add(result, 1);
|
||||
progress.setProgress(processedKeys++, totalKeys);
|
||||
} catch (NotFoundException | PgpGeneralException | IOException e) {
|
||||
Timber.e(e, "Error updating trust database");
|
||||
return new UpdateTrustResult(UpdateTrustResult.RESULT_ERROR, log);
|
||||
}
|
||||
log.add(result, 1);
|
||||
progress.setProgress(processedKeys++, totalKeys);
|
||||
} catch (NotFoundException | PgpGeneralException | IOException e) {
|
||||
Timber.e(e, "Error updating trust database");
|
||||
return new UpdateTrustResult(UpdateTrustResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
if (!isTrustDbInitialized) {
|
||||
preferences.setKeySignaturesTableInitialized();
|
||||
}
|
||||
|
||||
log.add(LogType.MSG_TRUST_OK, 1);
|
||||
return new UpdateTrustResult(UpdateTrustResult.RESULT_OK, log);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
if (!isTrustDbInitialized) {
|
||||
preferences.setKeySignaturesTableInitialized();
|
||||
}
|
||||
|
||||
log.add(LogType.MSG_TRUST_OK, 1);
|
||||
return new UpdateTrustResult(UpdateTrustResult.RESULT_OK, log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing
|
||||
*/
|
||||
private ContentProviderOperation
|
||||
buildCertOperations(long masterKeyId, int rank, WrappedSignature cert, int verified)
|
||||
buildCertOperations(long masterKeyId, int rank, WrappedSignature cert, VerificationStatus verificationStatus)
|
||||
throws IOException {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Certs.MASTER_KEY_ID, masterKeyId);
|
||||
@@ -1114,7 +1058,7 @@ public class KeyWritableRepository extends KeyRepository {
|
||||
values.put(Certs.KEY_ID_CERTIFIER, cert.getKeyId());
|
||||
values.put(Certs.TYPE, cert.getSignatureType());
|
||||
values.put(Certs.CREATION, cert.getCreationTime().getTime() / 1000);
|
||||
values.put(Certs.VERIFIED, verified);
|
||||
values.put(Certs.VERIFIED, CustomColumnAdapters.VERIFICATON_STATUS_ADAPTER.encode(verificationStatus));
|
||||
values.put(Certs.DATA, cert.getEncoded());
|
||||
|
||||
Uri uri = Certs.buildCertsUri(masterKeyId);
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
package org.sufficientlysecure.keychain.daos;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.daos;
|
||||
|
||||
|
||||
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 android.content.Context;
|
||||
|
||||
import okhttp3.internal.Util;
|
||||
|
||||
|
||||
public class LocalSecretKeyStorage {
|
||||
private static final String FORMAT_STR_SECRET_KEY = "0x%016x.sec";
|
||||
private static final String SECRET_KEYS_DIR_NAME = "secret_keys";
|
||||
|
||||
|
||||
private final File localSecretKeysDir;
|
||||
|
||||
|
||||
public static LocalSecretKeyStorage getInstance(Context context) {
|
||||
File localSecretKeysDir = new File(context.getFilesDir(), SECRET_KEYS_DIR_NAME);
|
||||
return new LocalSecretKeyStorage(localSecretKeysDir);
|
||||
}
|
||||
|
||||
private LocalSecretKeyStorage(File localSecretKeysDir) {
|
||||
this.localSecretKeysDir = localSecretKeysDir;
|
||||
}
|
||||
|
||||
private File getSecretKeyFile(long masterKeyId) throws IOException {
|
||||
if (!localSecretKeysDir.exists()) {
|
||||
localSecretKeysDir.mkdir();
|
||||
}
|
||||
if (!localSecretKeysDir.isDirectory()) {
|
||||
throw new IOException("Failed creating public key directory!");
|
||||
}
|
||||
|
||||
String keyFilename = String.format(FORMAT_STR_SECRET_KEY, masterKeyId);
|
||||
return new File(localSecretKeysDir, keyFilename);
|
||||
}
|
||||
|
||||
public void writeSecretKey(long masterKeyId, byte[] encoded) throws IOException {
|
||||
File publicKeyFile = getSecretKeyFile(masterKeyId);
|
||||
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(publicKeyFile);
|
||||
try {
|
||||
fileOutputStream.write(encoded);
|
||||
} finally {
|
||||
Util.closeQuietly(fileOutputStream);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] readSecretKey(long masterKeyId) throws IOException {
|
||||
File publicKeyFile = getSecretKeyFile(masterKeyId);
|
||||
|
||||
try {
|
||||
FileInputStream fileInputStream = new FileInputStream(publicKeyFile);
|
||||
return readIntoByteArray(fileInputStream);
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readIntoByteArray(FileInputStream fileInputStream) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
byte[] buf = new byte[128];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fileInputStream.read(buf)) != -1) {
|
||||
baos.write(buf, 0, bytesRead);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
void deleteSecretKey(long masterKeyId) throws IOException {
|
||||
File publicKeyFile = getSecretKeyFile(masterKeyId);
|
||||
if (publicKeyFile.exists()) {
|
||||
boolean deleteSuccess = publicKeyFile.delete();
|
||||
if (!deleteSuccess) {
|
||||
throw new IOException("File exists, but could not be deleted!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.daos;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.squareup.sqldelight.SqlDelightQuery;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.OverriddenWarningsModel.DeleteByIdentifier;
|
||||
import org.sufficientlysecure.keychain.OverriddenWarningsModel.InsertIdentifier;
|
||||
import org.sufficientlysecure.keychain.model.OverriddenWarning;
|
||||
|
||||
|
||||
public class OverriddenWarningsDao extends AbstractDao {
|
||||
public static OverriddenWarningsDao create(Context context) {
|
||||
KeychainDatabase database = KeychainDatabase.getInstance(context);
|
||||
DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
|
||||
return new OverriddenWarningsDao(database, databaseNotifyManager);
|
||||
}
|
||||
|
||||
private OverriddenWarningsDao(KeychainDatabase db, DatabaseNotifyManager databaseNotifyManager) {
|
||||
super(db, databaseNotifyManager);
|
||||
}
|
||||
|
||||
public boolean isWarningOverridden(String identifier) {
|
||||
SqlDelightQuery query = OverriddenWarning.FACTORY.selectCountByIdentifier(identifier);
|
||||
Long result = mapSingleRow(query, OverriddenWarning.FACTORY.selectCountByIdentifierMapper()::map);
|
||||
return result != null && result > 0;
|
||||
}
|
||||
|
||||
public void putOverride(String identifier) {
|
||||
InsertIdentifier statement = new InsertIdentifier(getWritableDb());
|
||||
statement.bind(identifier);
|
||||
statement.executeInsert();
|
||||
}
|
||||
|
||||
public void deleteOverride(String identifier) {
|
||||
DeleteByIdentifier statement = new DeleteByIdentifier(getWritableDb());
|
||||
statement.bind(identifier);
|
||||
statement.executeInsert();
|
||||
}
|
||||
}
|
||||
@@ -123,11 +123,13 @@ public class ImportKeysListLoader
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "IOException on parsing key file! Return NoValidKeysException!");
|
||||
OperationResult.OperationLog log = new OperationResult.OperationLog();
|
||||
log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0);
|
||||
GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log);
|
||||
mData.clear();
|
||||
mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult);
|
||||
if (mData.isEmpty()) {
|
||||
OperationResult.OperationLog log = new OperationResult.OperationLog();
|
||||
log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0);
|
||||
GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log);
|
||||
mData.clear();
|
||||
mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.keysync;
|
||||
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
|
||||
import androidx.work.Constraints.Builder;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class KeyserverSyncManager {
|
||||
private static final long SYNC_INTERVAL = 3;
|
||||
private static final TimeUnit SYNC_INTERVAL_UNIT = TimeUnit.DAYS;
|
||||
|
||||
private static final String PERIODIC_WORK_TAG = "keyserverSync";
|
||||
|
||||
public static void updateKeyserverSyncSchedule(Context context, boolean forceReschedule) {
|
||||
Preferences prefs = Preferences.getPreferences(context);
|
||||
if (!forceReschedule && prefs.isKeyserverSyncScheduled() != prefs.isKeyserverSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
WorkManager workManager = WorkManager.getInstance();
|
||||
if (workManager == null) {
|
||||
Timber.e("WorkManager unavailable!");
|
||||
return;
|
||||
}
|
||||
workManager.cancelAllWorkByTag(PERIODIC_WORK_TAG);
|
||||
|
||||
if (!prefs.isKeyserverSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Periodic syncs can't be unique, so we just use this to launch a uniquely queued worker */
|
||||
|
||||
Builder constraints = new Builder()
|
||||
.setRequiredNetworkType(prefs.getWifiOnlySync() ? NetworkType.UNMETERED : NetworkType.CONNECTED)
|
||||
.setRequiresBatteryNotLow(true);
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||
constraints.setRequiresDeviceIdle(true);
|
||||
}
|
||||
|
||||
PeriodicWorkRequest workRequest =
|
||||
new PeriodicWorkRequest.Builder(KeyserverSyncWorker.class, SYNC_INTERVAL, SYNC_INTERVAL_UNIT)
|
||||
.setConstraints(constraints.build())
|
||||
.addTag(PERIODIC_WORK_TAG)
|
||||
.build();
|
||||
workManager.enqueue(workRequest);
|
||||
|
||||
prefs.setKeyserverSyncScheduled(true);
|
||||
}
|
||||
|
||||
public static void debugRunSyncNow() {
|
||||
WorkManager workManager = WorkManager.getInstance();
|
||||
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(KeyserverSyncWorker.class).build();
|
||||
workManager.enqueue(workRequest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package org.sufficientlysecure.keychain.keysync;
|
||||
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationCompat.Builder;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import androidx.work.Worker;
|
||||
import org.sufficientlysecure.keychain.Constants.NotificationChannels;
|
||||
import org.sufficientlysecure.keychain.Constants.NotificationIds;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
|
||||
import org.sufficientlysecure.keychain.operations.KeySyncOperation;
|
||||
import org.sufficientlysecure.keychain.operations.KeySyncParcel;
|
||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
|
||||
import org.sufficientlysecure.keychain.util.ResourceUtils;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class KeyserverSyncWorker extends Worker {
|
||||
private CancellationSignal cancellationSignal = new CancellationSignal();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public WorkerResult doWork() {
|
||||
KeyWritableRepository keyWritableRepository = KeyWritableRepository.create(getApplicationContext());
|
||||
|
||||
Timber.d("Starting key sync…");
|
||||
Progressable notificationProgressable = notificationShowForProgress();
|
||||
KeySyncOperation keySync = new KeySyncOperation(getApplicationContext(), keyWritableRepository, notificationProgressable, cancellationSignal);
|
||||
ImportKeyResult result = keySync.execute(KeySyncParcel.createRefreshOutdated(), CryptoInputParcel.createCryptoInputParcel());
|
||||
return handleUpdateResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we're returning START_REDELIVER_INTENT in onStartCommand, we need to remember to call
|
||||
* stopSelf(int) to prevent the Intent from being redelivered if our work is already done
|
||||
*
|
||||
* @param result
|
||||
* result of keyserver sync
|
||||
*/
|
||||
private WorkerResult handleUpdateResult(ImportKeyResult result) {
|
||||
if (result.isPending()) {
|
||||
Timber.d("Orbot required for sync but not running, attempting to start");
|
||||
// result is pending due to Orbot not being started
|
||||
// try to start it silently, if disabled show notifications
|
||||
new OrbotHelper.SilentStartManager() {
|
||||
@Override
|
||||
protected void onOrbotStarted() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSilentStartDisabled() {
|
||||
OrbotRequiredDialogActivity.showOrbotRequiredNotification(getApplicationContext());
|
||||
}
|
||||
}.startOrbotAndListen(getApplicationContext(), false);
|
||||
return WorkerResult.RETRY;
|
||||
} else if (isStopped()) {
|
||||
Timber.d("Keyserver sync cancelled");
|
||||
return WorkerResult.FAILURE;
|
||||
} else {
|
||||
Timber.d("Keyserver sync completed: Updated: %d, Failed: %d", result.mUpdatedKeys, result.mBadKeys);
|
||||
return WorkerResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
private Progressable notificationShowForProgress() {
|
||||
final Context context = getApplicationContext();
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
createNotificationChannelsIfNecessary(context, notificationManager);
|
||||
|
||||
NotificationCompat.Builder builder = new Builder(context, NotificationChannels.KEYSERVER_SYNC)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_24dp)
|
||||
.setLargeIcon(ResourceUtils.getDrawableAsNotificationBitmap(context, R.mipmap.ic_launcher))
|
||||
.setContentTitle(context.getString(R.string.notify_title_keysync))
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setTimeoutAfter(5000)
|
||||
.setVibrate(null)
|
||||
.setSound(null)
|
||||
.setProgress(0, 0, true);
|
||||
|
||||
return new Progressable() {
|
||||
@Override
|
||||
public void setProgress(String message, int current, int total) {
|
||||
setProgress(current, total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProgress(int resourceId, int current, int total) {
|
||||
setProgress(current, total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProgress(int current, int total) {
|
||||
if (total == 0) {
|
||||
notificationManager.cancel(NotificationIds.KEYSERVER_SYNC);
|
||||
return;
|
||||
}
|
||||
|
||||
builder.setProgress(total, current, false);
|
||||
if (current == total) {
|
||||
builder.setContentTitle(context.getString(R.string.notify_title_keysync_finished, total));
|
||||
builder.setContentText(null);
|
||||
} else {
|
||||
builder.setContentText(context.getString(R.string.notify_content_keysync, current, total));
|
||||
}
|
||||
notificationManager.notify(NotificationIds.KEYSERVER_SYNC, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPreventCancel() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void createNotificationChannelsIfNecessary(Context context,
|
||||
NotificationManager notificationManager) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = context.getString(R.string.notify_channel_keysync);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
NotificationChannels.KEYSERVER_SYNC, name, NotificationManager.IMPORTANCE_MIN);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
super.onStopped();
|
||||
cancellationSignal.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.sufficientlysecure.keychain.livedata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager;
|
||||
import org.sufficientlysecure.keychain.livedata.ApiAppsLiveData.ListedApp;
|
||||
import org.sufficientlysecure.keychain.model.ApiApp;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData;
|
||||
|
||||
|
||||
public class ApiAppsLiveData extends AsyncTaskLiveData<List<ListedApp>> {
|
||||
private final ApiAppDao apiAppDao;
|
||||
private final PackageManager packageManager;
|
||||
|
||||
public ApiAppsLiveData(Context context) {
|
||||
super(context, DatabaseNotifyManager.getNotifyUriAllApps());
|
||||
|
||||
packageManager = getContext().getPackageManager();
|
||||
apiAppDao = ApiAppDao.getInstance(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ListedApp> asyncLoadData() {
|
||||
ArrayList<ListedApp> result = new ArrayList<>();
|
||||
|
||||
loadRegisteredApps(result);
|
||||
addPlaceholderApps(result);
|
||||
|
||||
Collections.sort(result, (o1, o2) -> o1.readableName.compareTo(o2.readableName));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void loadRegisteredApps(ArrayList<ListedApp> result) {
|
||||
List<ApiApp> registeredApiApps = apiAppDao.getAllApiApps();
|
||||
|
||||
for (ApiApp apiApp : registeredApiApps) {
|
||||
ListedApp listedApp;
|
||||
try {
|
||||
ApplicationInfo ai = packageManager.getApplicationInfo(apiApp.package_name(), 0);
|
||||
CharSequence applicationLabel = packageManager.getApplicationLabel(ai);
|
||||
Drawable applicationIcon = packageManager.getApplicationIcon(ai);
|
||||
|
||||
listedApp = new ListedApp(apiApp.package_name(), true, true, applicationLabel, applicationIcon, null);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
listedApp = new ListedApp(apiApp.package_name(), false, true, apiApp.package_name(), null, null);
|
||||
}
|
||||
result.add(listedApp);
|
||||
}
|
||||
}
|
||||
|
||||
private void addPlaceholderApps(ArrayList<ListedApp> result) {
|
||||
for (ListedApp placeholderApp : PLACERHOLDER_APPS) {
|
||||
if (!containsByPackageName(result, placeholderApp.packageName)) {
|
||||
try {
|
||||
packageManager.getApplicationInfo(placeholderApp.packageName, 0);
|
||||
result.add(placeholderApp.withIsInstalled());
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
result.add(placeholderApp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsByPackageName(ArrayList<ListedApp> result, String packageName) {
|
||||
for (ListedApp app : result) {
|
||||
if (packageName.equals(app.packageName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static class ListedApp {
|
||||
public final String packageName;
|
||||
public final boolean isInstalled;
|
||||
public final boolean isRegistered;
|
||||
public final String readableName;
|
||||
public final Drawable applicationIcon;
|
||||
public final Integer applicationIconRes;
|
||||
|
||||
ListedApp(String packageName, boolean isInstalled, boolean isRegistered, CharSequence readableName,
|
||||
Drawable applicationIcon, Integer applicationIconRes) {
|
||||
this.packageName = packageName;
|
||||
this.isInstalled = isInstalled;
|
||||
this.isRegistered = isRegistered;
|
||||
this.readableName = readableName.toString();
|
||||
this.applicationIcon = applicationIcon;
|
||||
this.applicationIconRes = applicationIconRes;
|
||||
}
|
||||
|
||||
public ListedApp withIsInstalled() {
|
||||
return new ListedApp(packageName, true, isRegistered, readableName, applicationIcon, applicationIconRes);
|
||||
}
|
||||
}
|
||||
|
||||
private static final ListedApp[] PLACERHOLDER_APPS = {
|
||||
new ListedApp("com.fsck.k9", false, false, "K-9 Mail", null, R.drawable.apps_k9),
|
||||
new ListedApp("com.zeapo.pwdstore", false, false, "Password Store", null, R.drawable.apps_password_store),
|
||||
new ListedApp("eu.siacs.conversations", false, false, "Conversations (Instant Messaging)", null,
|
||||
R.drawable.apps_conversations)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.sufficientlysecure.keychain.livedata;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData;
|
||||
|
||||
|
||||
public class GenericLiveData<T> extends AsyncTaskLiveData<T> {
|
||||
private GenericDataLoader<T> genericDataLoader;
|
||||
|
||||
public GenericLiveData(Context context, GenericDataLoader<T> genericDataLoader) {
|
||||
super(context, null);
|
||||
this.genericDataLoader = genericDataLoader;
|
||||
}
|
||||
|
||||
public GenericLiveData(Context context, Uri notifyUri, GenericDataLoader<T> genericDataLoader) {
|
||||
super(context, notifyUri);
|
||||
this.genericDataLoader = genericDataLoader;
|
||||
}
|
||||
|
||||
public GenericLiveData(Context context, long notifyMasterKeyId, GenericDataLoader<T> genericDataLoader) {
|
||||
super(context, DatabaseNotifyManager.getNotifyUriMasterKeyId(notifyMasterKeyId));
|
||||
this.genericDataLoader = genericDataLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T asyncLoadData() {
|
||||
return genericDataLoader.loadData();
|
||||
}
|
||||
|
||||
public interface GenericDataLoader<T> {
|
||||
T loadData();
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.livedata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.util.DatabaseUtil;
|
||||
|
||||
|
||||
public class KeyInfoInteractor {
|
||||
// These are the rows that we will retrieve.
|
||||
private String[] QUERY_PROJECTION = new String[]{
|
||||
KeyRings._ID,
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
KeyRings.CREATION,
|
||||
KeyRings.HAS_ENCRYPT,
|
||||
KeyRings.HAS_AUTHENTICATE_SECRET,
|
||||
KeyRings.HAS_ANY_SECRET,
|
||||
KeyRings.VERIFIED,
|
||||
KeyRings.NAME,
|
||||
KeyRings.EMAIL,
|
||||
KeyRings.COMMENT,
|
||||
KeyRings.IS_EXPIRED,
|
||||
KeyRings.IS_REVOKED,
|
||||
};
|
||||
private static final int INDEX_MASTER_KEY_ID = 1;
|
||||
private static final int INDEX_CREATION = 2;
|
||||
private static final int INDEX_HAS_ENCRYPT = 3;
|
||||
private static final int INDEX_HAS_AUTHENTICATE = 4;
|
||||
private static final int INDEX_HAS_ANY_SECRET = 5;
|
||||
private static final int INDEX_VERIFIED = 6;
|
||||
private static final int INDEX_NAME = 7;
|
||||
private static final int INDEX_EMAIL = 8;
|
||||
private static final int INDEX_COMMENT = 9;
|
||||
|
||||
private static final String QUERY_WHERE_ALL = Tables.KEYS + "." + KeyRings.IS_REVOKED +
|
||||
" = 0 AND " + KeyRings.IS_EXPIRED + " = 0";
|
||||
private static final String QUERY_WHERE_SECRET = Tables.KEYS + "." + KeyRings.IS_REVOKED +
|
||||
" = 0 AND " + KeyRings.IS_EXPIRED + " = 0" + " AND " + KeyRings.HAS_ANY_SECRET + " != 0";
|
||||
private static final String QUERY_ORDER = Tables.KEYS + "." + KeyRings.CREATION + " DESC";
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
|
||||
|
||||
public KeyInfoInteractor(ContentResolver contentResolver) {
|
||||
this.contentResolver = contentResolver;
|
||||
}
|
||||
|
||||
public List<KeyInfo> loadKeyInfo(KeySelector keySelector) {
|
||||
ArrayList<KeyInfo> keyInfos = new ArrayList<>();
|
||||
Cursor cursor;
|
||||
|
||||
String selection = keySelector.isOnlySecret() ? QUERY_WHERE_SECRET : QUERY_WHERE_ALL;
|
||||
String additionalSelection = keySelector.getSelection();
|
||||
|
||||
selection = DatabaseUtil.concatenateWhere(selection, additionalSelection);
|
||||
cursor = contentResolver.query(keySelector.getKeyRingUri(), QUERY_PROJECTION, selection, null, QUERY_ORDER);
|
||||
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
KeyInfo keyInfo = KeyInfo.fromCursor(cursor);
|
||||
keyInfos.add(keyInfo);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(keyInfos);
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
public abstract static class KeyInfo {
|
||||
public abstract long getMasterKeyId();
|
||||
public abstract long getCreationDate();
|
||||
public abstract boolean getHasEncrypt();
|
||||
public abstract boolean getHasAuthenticate();
|
||||
public abstract boolean getHasAnySecret();
|
||||
public abstract boolean getIsVerified();
|
||||
|
||||
@Nullable
|
||||
public abstract String getName();
|
||||
@Nullable
|
||||
public abstract String getEmail();
|
||||
@Nullable
|
||||
public abstract String getComment();
|
||||
|
||||
static KeyInfo fromCursor(Cursor cursor) {
|
||||
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
long creationDate = cursor.getLong(INDEX_CREATION) * 1000L;
|
||||
boolean hasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
|
||||
boolean hasAuthenticate = cursor.getInt(INDEX_HAS_AUTHENTICATE) != 0;
|
||||
boolean hasAnySecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
|
||||
boolean isVerified = cursor.getInt(INDEX_VERIFIED) == 2;
|
||||
|
||||
String name = cursor.getString(INDEX_NAME);
|
||||
String email = cursor.getString(INDEX_EMAIL);
|
||||
String comment = cursor.getString(INDEX_COMMENT);
|
||||
|
||||
return new AutoValue_KeyInfoInteractor_KeyInfo(
|
||||
masterKeyId, creationDate, hasEncrypt, hasAuthenticate, hasAnySecret, isVerified, name, email, comment);
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
public abstract static class KeySelector {
|
||||
public abstract Uri getKeyRingUri();
|
||||
@Nullable
|
||||
public abstract String getSelection();
|
||||
public abstract boolean isOnlySecret();
|
||||
|
||||
public static KeySelector create(Uri keyRingUri, String selection) {
|
||||
return new AutoValue_KeyInfoInteractor_KeySelector(keyRingUri, selection, false);
|
||||
}
|
||||
|
||||
public static KeySelector createOnlySecret(Uri keyRingUri, String selection) {
|
||||
return new AutoValue_KeyInfoInteractor_KeySelector(keyRingUri, selection, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package org.sufficientlysecure.keychain.livedata;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
|
||||
import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo;
|
||||
import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData;
|
||||
|
||||
|
||||
public class KeyInfoLiveData extends AsyncTaskLiveData<List<KeyInfo>> {
|
||||
private final KeyInfoInteractor keyInfoInteractor;
|
||||
|
||||
private KeySelector keySelector;
|
||||
|
||||
public KeyInfoLiveData(Context context, ContentResolver contentResolver) {
|
||||
super(context, null);
|
||||
|
||||
this.keyInfoInteractor = new KeyInfoInteractor(contentResolver);
|
||||
}
|
||||
|
||||
public void setKeySelector(KeySelector keySelector) {
|
||||
this.keySelector = keySelector;
|
||||
|
||||
updateDataInBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<KeyInfo> asyncLoadData() {
|
||||
if (keySelector == null) {
|
||||
return null;
|
||||
}
|
||||
return keyInfoInteractor.loadKeyInfo(keySelector);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.ApiAllowedKeysModel;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class ApiAllowedKey implements ApiAllowedKeysModel {
|
||||
public static final Factory<ApiAllowedKey> FACTORY = new Factory<ApiAllowedKey>(AutoValue_ApiAllowedKey::new);
|
||||
}
|
||||
@@ -15,23 +15,19 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.compatibility;
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
/**
|
||||
* Bug on Android >= 4.1
|
||||
* <p/>
|
||||
* http://code.google.com/p/android/issues/detail?id=35885
|
||||
* <p/>
|
||||
* Items are not checked in layout
|
||||
*/
|
||||
public class ListFragmentWorkaround extends ListFragment {
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.ApiAppsModel;
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
||||
l.setItemChecked(position, l.isItemChecked(position));
|
||||
|
||||
@AutoValue
|
||||
public abstract class ApiApp implements ApiAppsModel {
|
||||
public static final ApiAppsModel.Factory<ApiApp> FACTORY =
|
||||
new ApiAppsModel.Factory<ApiApp>(AutoValue_ApiApp::new);
|
||||
|
||||
public static ApiApp create(String packageName, byte[] packageSignature) {
|
||||
return new AutoValue_ApiApp(null, packageName, packageSignature);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.squareup.sqldelight.RowMapper;
|
||||
import org.sufficientlysecure.keychain.AutocryptPeersModel;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class AutocryptPeer implements AutocryptPeersModel {
|
||||
|
||||
public enum GossipOrigin {
|
||||
GOSSIP_HEADER, SIGNATURE, DEDUP
|
||||
}
|
||||
|
||||
public static final Factory<AutocryptPeer> FACTORY = new Factory<AutocryptPeer>(AutoValue_AutocryptPeer::new,
|
||||
CustomColumnAdapters.DATE_ADAPTER, CustomColumnAdapters.DATE_ADAPTER, CustomColumnAdapters.DATE_ADAPTER,
|
||||
CustomColumnAdapters.GOSSIP_ORIGIN_ADAPTER);
|
||||
|
||||
public static final RowMapper<AutocryptPeer> PEER_MAPPER = FACTORY.selectByIdentifiersMapper();
|
||||
public static final RowMapper<AutocryptKeyStatus> KEY_STATUS_MAPPER =
|
||||
FACTORY.selectAutocryptKeyStatusMapper(AutoValue_AutocryptPeer_AutocryptKeyStatus::new);
|
||||
|
||||
@AutoValue
|
||||
public static abstract class AutocryptKeyStatus implements SelectAutocryptKeyStatusModel<AutocryptPeer> {
|
||||
public boolean hasGossipKey() {
|
||||
return autocryptPeer().gossip_master_key_id() != null;
|
||||
}
|
||||
|
||||
public boolean isGossipKeyRevoked() {
|
||||
Boolean gossip_key_is_revoked = gossip_key_is_revoked_int();
|
||||
return gossip_key_is_revoked != null && gossip_key_is_revoked;
|
||||
}
|
||||
|
||||
public boolean isGossipKeyExpired() {
|
||||
return gossip_key_is_expired_int() != 0;
|
||||
}
|
||||
|
||||
public boolean isGossipKeyVerified() {
|
||||
return gossip_key_is_verified_int() != 0;
|
||||
}
|
||||
|
||||
public boolean isKeyRevoked() {
|
||||
Boolean revoked = key_is_revoked_int();
|
||||
return revoked != null && revoked;
|
||||
}
|
||||
|
||||
public boolean isKeyExpired() {
|
||||
return key_is_expired_int() != 0;
|
||||
}
|
||||
|
||||
public boolean isKeyVerified() {
|
||||
return key_is_verified_int() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.CertsModel;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class Certification implements CertsModel {
|
||||
public static final CertsModel.Factory<Certification> FACTORY =
|
||||
new CertsModel.Factory<>(AutoValue_Certification::new, CustomColumnAdapters.VERIFICATON_STATUS_ADAPTER);
|
||||
|
||||
public static final SelectVerifyingCertDetailsMapper<CertDetails> CERT_DETAILS_MAPPER =
|
||||
new SelectVerifyingCertDetailsMapper<>(AutoValue_Certification_CertDetails::new);
|
||||
|
||||
@AutoValue
|
||||
public static abstract class CertDetails implements CertsModel.SelectVerifyingCertDetailsModel {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.squareup.sqldelight.ColumnAdapter;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
|
||||
|
||||
public final class CustomColumnAdapters {
|
||||
|
||||
private CustomColumnAdapters() { }
|
||||
|
||||
static final ColumnAdapter<Date,Long> DATE_ADAPTER = new ColumnAdapter<Date,Long>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public Date decode(Long databaseValue) {
|
||||
// Both SQLite and OpenPGP prefer a second granularity for timestamps - so we'll translate here
|
||||
return new Date(databaseValue * 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encode(@NonNull Date value) {
|
||||
return value.getTime() / 1000;
|
||||
}
|
||||
};
|
||||
|
||||
static final ColumnAdapter<GossipOrigin,Long> GOSSIP_ORIGIN_ADAPTER = new ColumnAdapter<GossipOrigin,Long>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public GossipOrigin decode(Long databaseValue) {
|
||||
switch (databaseValue.intValue()) {
|
||||
case 0: return GossipOrigin.GOSSIP_HEADER;
|
||||
case 10: return GossipOrigin.SIGNATURE;
|
||||
case 20: return GossipOrigin.DEDUP;
|
||||
default: throw new IllegalArgumentException("Unhandled database value!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encode(@NonNull GossipOrigin value) {
|
||||
switch (value) {
|
||||
case GOSSIP_HEADER: return 0L;
|
||||
case SIGNATURE: return 10L;
|
||||
case DEDUP: return 20L;
|
||||
default: throw new IllegalArgumentException("Unhandled database value!");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static final ColumnAdapter<SecretKeyType,Long> SECRET_KEY_TYPE_ADAPTER = new ColumnAdapter<SecretKeyType, Long>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public SecretKeyType decode(Long databaseValue) {
|
||||
return databaseValue == null ? SecretKeyType.UNAVAILABLE : SecretKeyType.fromNum(databaseValue.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encode(@NonNull SecretKeyType value) {
|
||||
return (long) value.getNum();
|
||||
}
|
||||
};
|
||||
|
||||
public static final ColumnAdapter<VerificationStatus,Long> VERIFICATON_STATUS_ADAPTER = new ColumnAdapter<VerificationStatus, Long>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public VerificationStatus decode(Long databaseValue) {
|
||||
if (databaseValue == null) {
|
||||
return VerificationStatus.UNVERIFIED;
|
||||
}
|
||||
switch (databaseValue.intValue()) {
|
||||
case 0: return VerificationStatus.UNVERIFIED;
|
||||
case 1: return VerificationStatus.VERIFIED_SECRET;
|
||||
case 2: return VerificationStatus.VERIFIED_SELF;
|
||||
default: throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encode(@NonNull VerificationStatus value) {
|
||||
switch (value) {
|
||||
case UNVERIFIED: return 0L;
|
||||
case VERIFIED_SECRET: return 1L;
|
||||
case VERIFIED_SELF: return 2L;
|
||||
default: throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.KeyMetadataModel;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class KeyMetadata implements KeyMetadataModel {
|
||||
public static final Factory<KeyMetadata> FACTORY = new Factory<>(
|
||||
AutoValue_KeyMetadata::new, CustomColumnAdapters.DATE_ADAPTER);
|
||||
|
||||
public boolean hasBeenUpdated() {
|
||||
return last_updated() != null;
|
||||
}
|
||||
|
||||
public boolean isPublished() {
|
||||
if (last_updated() == null) {
|
||||
throw new IllegalStateException("Cannot get publication state if key has never been updated!");
|
||||
}
|
||||
Boolean seenOnKeyservers = seen_on_keyservers();
|
||||
return seenOnKeyservers != null && seenOnKeyservers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.KeyRingsPublicModel;
|
||||
|
||||
@AutoValue
|
||||
public abstract class KeyRingPublic implements KeyRingsPublicModel {
|
||||
public static final Factory<KeyRingPublic> FACTORY = new Factory<>(AutoValue_KeyRingPublic::new);
|
||||
|
||||
public static final Mapper<KeyRingPublic> MAPPER = new Mapper<>(FACTORY);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.KeySignaturesModel;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class KeySignature implements KeySignaturesModel {
|
||||
public static final Factory<KeySignature> FACTORY = new Factory<>(AutoValue_KeySignature::new);
|
||||
|
||||
public static final Mapper<KeySignature> MAPPER = new Mapper<>(FACTORY);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.KeySignaturesModel;
|
||||
import org.sufficientlysecure.keychain.OverriddenWarningsModel;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class OverriddenWarning implements OverriddenWarningsModel {
|
||||
public static final Factory<OverriddenWarning> FACTORY = new Factory<>(AutoValue_OverriddenWarning::new);
|
||||
|
||||
public static final Mapper<OverriddenWarning> MAPPER = new Mapper<>(FACTORY);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.squareup.sqldelight.RowMapper;
|
||||
import org.sufficientlysecure.keychain.KeysModel;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class SubKey implements KeysModel {
|
||||
public static final Factory<SubKey> FACTORY =
|
||||
new Factory<>(AutoValue_SubKey::new, CustomColumnAdapters.SECRET_KEY_TYPE_ADAPTER);
|
||||
public static final UnifiedKeyViewMapper<UnifiedKeyInfo, Certification> UNIFIED_KEY_INFO_MAPPER =
|
||||
FACTORY.selectAllUnifiedKeyInfoMapper(
|
||||
AutoValue_SubKey_UnifiedKeyInfo::new, Certification.FACTORY);
|
||||
public static Mapper<SubKey> SUBKEY_MAPPER = new Mapper<>(FACTORY);
|
||||
public static RowMapper<SecretKeyType> SKT_MAPPER = FACTORY.selectSecretKeyTypeMapper();
|
||||
|
||||
public boolean expires() {
|
||||
return expiry() != null;
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
public static abstract class UnifiedKeyInfo implements KeysModel.UnifiedKeyViewModel {
|
||||
private List<String> autocryptPackageNames;
|
||||
private String cachedUidSearchString;
|
||||
|
||||
public boolean is_expired() {
|
||||
Long expiry = expiry();
|
||||
return expiry != null && expiry * 1000 < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean has_any_secret() {
|
||||
return has_any_secret_int() != 0;
|
||||
}
|
||||
|
||||
public boolean is_verified() {
|
||||
VerificationStatus verified = verified();
|
||||
return verified != null && verified == VerificationStatus.VERIFIED_SECRET;
|
||||
}
|
||||
|
||||
public boolean has_duplicate() {
|
||||
return has_duplicate_int() != 0;
|
||||
}
|
||||
|
||||
public List<String> autocrypt_package_names() {
|
||||
if (autocryptPackageNames == null) {
|
||||
String csv = autocrypt_package_names_csv();
|
||||
autocryptPackageNames = csv == null ? Collections.emptyList() :
|
||||
Arrays.asList(csv.split(","));
|
||||
}
|
||||
return autocryptPackageNames;
|
||||
}
|
||||
|
||||
public boolean has_auth_key() {
|
||||
return has_auth_key_int() != 0;
|
||||
}
|
||||
|
||||
public boolean has_encrypt_key() {
|
||||
return has_encrypt_key_int() != 0;
|
||||
}
|
||||
|
||||
public boolean has_sign_key() {
|
||||
return has_sign_key_int() != 0;
|
||||
}
|
||||
|
||||
public String uidSearchString() {
|
||||
if (cachedUidSearchString == null) {
|
||||
cachedUidSearchString = user_id_list();
|
||||
if (cachedUidSearchString == null) {
|
||||
cachedUidSearchString = "";
|
||||
}
|
||||
cachedUidSearchString = cachedUidSearchString.toLowerCase();
|
||||
}
|
||||
return cachedUidSearchString;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.sufficientlysecure.keychain.model;
|
||||
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import org.sufficientlysecure.keychain.UserPacketsModel;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class UserPacket implements UserPacketsModel {
|
||||
public static final Factory<UserPacket> FACTORY = new Factory<>(AutoValue_UserPacket::new);
|
||||
public static final SelectUserIdsByMasterKeyIdMapper<UserId, Certification> USER_ID_MAPPER =
|
||||
FACTORY.selectUserIdsByMasterKeyIdMapper(AutoValue_UserPacket_UserId::new, Certification.FACTORY);
|
||||
public static final SelectUserAttributesByTypeAndMasterKeyIdMapper<UserAttribute, Certification> USER_ATTRIBUTE_MAPPER =
|
||||
FACTORY.selectUserAttributesByTypeAndMasterKeyIdMapper(AutoValue_UserPacket_UserAttribute::new, Certification.FACTORY);
|
||||
|
||||
@AutoValue
|
||||
public static abstract class UserId implements SelectUserIdsByMasterKeyIdModel {
|
||||
public boolean isVerified() {
|
||||
return verified() == VerificationStatus.VERIFIED_SECRET;
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
public static abstract class UserAttribute implements SelectUserAttributesByTypeAndMasterKeyIdModel {
|
||||
public boolean isVerified() {
|
||||
return verified() == VerificationStatus.VERIFIED_SECRET;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
|
||||
import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class NetworkReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
ConnectivityManager conn = (ConnectivityManager)
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = conn.getActiveNetworkInfo();
|
||||
boolean isTypeWifi = (networkInfo != null) &&
|
||||
(networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
|
||||
boolean isConnected = (networkInfo != null) && networkInfo.isConnected();
|
||||
|
||||
if (isTypeWifi && isConnected) {
|
||||
|
||||
// broadcaster receiver disabled
|
||||
setWifiReceiverComponent(false, context);
|
||||
Intent serviceIntent = new Intent(context, KeyserverSyncAdapterService.class);
|
||||
serviceIntent.setAction(KeyserverSyncAdapterService.ACTION_SYNC_NOW);
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
public void setWifiReceiverComponent(Boolean isEnabled, Context context) {
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
ComponentName compName = new ComponentName(context,
|
||||
NetworkReceiver.class);
|
||||
|
||||
if (isEnabled) {
|
||||
pm.setComponentEnabledSetting(compName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
|
||||
Timber.d("Wifi Receiver is enabled!");
|
||||
} else {
|
||||
pm.setComponentEnabledSetting(compName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
Timber.d("Wifi Receiver is disabled!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,23 +24,21 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
||||
import org.sufficientlysecure.keychain.operations.results.ExportResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
@@ -52,17 +50,15 @@ import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
|
||||
import org.sufficientlysecure.keychain.service.BackupKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Numeric9x4PassphraseUtil;
|
||||
import org.sufficientlysecure.keychain.util.CountingOutputStream;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.Numeric9x4PassphraseUtil;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import timber.log.Timber;
|
||||
|
||||
@@ -78,14 +74,6 @@ import timber.log.Timber;
|
||||
* either the name of a file or an output uri to write to.
|
||||
*/
|
||||
public class BackupOperation extends BaseOperation<BackupKeyringParcel> {
|
||||
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
KeyRings.HAS_ANY_SECRET
|
||||
};
|
||||
private static final int INDEX_MASTER_KEY_ID = 0;
|
||||
private static final int INDEX_HAS_ANY_SECRET = 1;
|
||||
|
||||
// this is a very simple matcher, we only need basic sanitization
|
||||
private static final Pattern HEADER_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+: [^\\n]+");
|
||||
|
||||
@@ -95,7 +83,7 @@ public class BackupOperation extends BaseOperation<BackupKeyringParcel> {
|
||||
}
|
||||
|
||||
public BackupOperation(Context context, KeyRepository keyRepository,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, keyRepository, progressable, cancelled);
|
||||
}
|
||||
|
||||
@@ -223,42 +211,36 @@ public class BackupOperation extends BaseOperation<BackupKeyringParcel> {
|
||||
OutputStream outStream, List<String> extraSecretKeyHeaders) {
|
||||
// noinspection unused TODO use these in a log entry
|
||||
int okSecret = 0, okPublic = 0;
|
||||
|
||||
int progress = 0;
|
||||
|
||||
Cursor cursor = queryForKeys(masterKeyIds);
|
||||
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
log.add(LogType.MSG_BACKUP_ERROR_DB, 1);
|
||||
return false; // new ExportResult(ExportResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
int numKeys = cursor.getCount();
|
||||
List<UnifiedKeyInfo> unifiedKeyInfos;
|
||||
if (masterKeyIds == null) {
|
||||
unifiedKeyInfos = mKeyRepository.getAllUnifiedKeyInfo();
|
||||
} else {
|
||||
unifiedKeyInfos = mKeyRepository.getUnifiedKeyInfo(masterKeyIds);
|
||||
}
|
||||
int numKeys = unifiedKeyInfos.size();
|
||||
|
||||
updateProgress(mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, numKeys),
|
||||
0, numKeys);
|
||||
|
||||
// For each public masterKey id
|
||||
while (!cursor.isAfterLast()) {
|
||||
|
||||
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
log.add(LogType.MSG_BACKUP_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(masterKeyId));
|
||||
for (UnifiedKeyInfo keyInfo : unifiedKeyInfos) {
|
||||
log.add(LogType.MSG_BACKUP_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyInfo.master_key_id()));
|
||||
|
||||
boolean publicKeyWriteOk = false;
|
||||
if (exportPublic) {
|
||||
publicKeyWriteOk = writePublicKeyToStream(masterKeyId, log, outStream);
|
||||
publicKeyWriteOk = writePublicKeyToStream(keyInfo.master_key_id(), log, outStream);
|
||||
if (publicKeyWriteOk) {
|
||||
okPublic += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (publicKeyWriteOk || !exportPublic) {
|
||||
boolean hasSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) > 0;
|
||||
if (exportSecret && hasSecret) {
|
||||
log.add(LogType.MSG_BACKUP_SECRET, 2, KeyFormattingUtils.beautifyKeyId(masterKeyId));
|
||||
if (writeSecretKeyToStream(masterKeyId, log, outStream, extraSecretKeyHeaders)) {
|
||||
if (exportSecret && keyInfo.has_any_secret()) {
|
||||
log.add(LogType.MSG_BACKUP_SECRET, 2, KeyFormattingUtils.beautifyKeyId(keyInfo.master_key_id()));
|
||||
if (writeSecretKeyToStream(keyInfo.master_key_id(), log, outStream, extraSecretKeyHeaders)) {
|
||||
okSecret += 1;
|
||||
}
|
||||
extraSecretKeyHeaders = null;
|
||||
@@ -266,7 +248,6 @@ public class BackupOperation extends BaseOperation<BackupKeyringParcel> {
|
||||
}
|
||||
|
||||
updateProgress(progress++, numKeys);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
|
||||
updateProgress(R.string.progress_done, numKeys, numKeys);
|
||||
@@ -281,7 +262,6 @@ public class BackupOperation extends BaseOperation<BackupKeyringParcel> {
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "error closing stream");
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -341,29 +321,4 @@ public class BackupOperation extends BaseOperation<BackupKeyringParcel> {
|
||||
}
|
||||
}
|
||||
|
||||
private Cursor queryForKeys(long[] masterKeyIds) {
|
||||
String selection = null, selectionArgs[] = null;
|
||||
|
||||
if (masterKeyIds != null) {
|
||||
// convert long[] to String[]
|
||||
selectionArgs = new String[masterKeyIds.length];
|
||||
for (int i = 0; i < masterKeyIds.length; i++) {
|
||||
selectionArgs[i] = Long.toString(masterKeyIds[i]);
|
||||
}
|
||||
|
||||
// generates ?,?,? as placeholders for selectionArgs
|
||||
String placeholders = TextUtils.join(",",
|
||||
Collections.nCopies(masterKeyIds.length, "?"));
|
||||
|
||||
// put together selection string
|
||||
selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID
|
||||
+ " IN (" + placeholders + ")";
|
||||
}
|
||||
|
||||
return mKeyRepository.getContentResolver().query(
|
||||
KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, selection, selectionArgs,
|
||||
Tables.KEYS + "." + KeyRings.MASTER_KEY_ID
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,19 +18,17 @@
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants.key;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
@@ -39,7 +37,7 @@ public abstract class BaseOperation<T extends Parcelable> implements PassphraseC
|
||||
|
||||
final public Context mContext;
|
||||
final public Progressable mProgressable;
|
||||
final public AtomicBoolean mCancelled;
|
||||
final public CancellationSignal mCancelled;
|
||||
|
||||
final public KeyRepository mKeyRepository;
|
||||
|
||||
@@ -73,7 +71,7 @@ public abstract class BaseOperation<T extends Parcelable> implements PassphraseC
|
||||
}
|
||||
|
||||
public BaseOperation(Context context, KeyRepository keyRepository,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
mContext = context;
|
||||
mProgressable = progressable;
|
||||
mKeyRepository = keyRepository;
|
||||
@@ -102,7 +100,7 @@ public abstract class BaseOperation<T extends Parcelable> implements PassphraseC
|
||||
}
|
||||
|
||||
protected boolean checkCancelled() {
|
||||
return mCancelled != null && mCancelled.get();
|
||||
return mCancelled != null && mCancelled.isCanceled();
|
||||
}
|
||||
|
||||
protected void setPreventCancel () {
|
||||
@@ -113,15 +111,14 @@ public abstract class BaseOperation<T extends Parcelable> implements PassphraseC
|
||||
|
||||
@Override
|
||||
public Passphrase getCachedPassphrase(long subKeyId) throws NoSecretKeyException {
|
||||
try {
|
||||
if (subKeyId != key.symmetric) {
|
||||
long masterKeyId = mKeyRepository.getMasterKeyId(subKeyId);
|
||||
return getCachedPassphrase(masterKeyId, subKeyId);
|
||||
if (subKeyId != key.symmetric) {
|
||||
Long masterKeyId = mKeyRepository.getMasterKeyIdBySubkeyId(subKeyId);
|
||||
if (masterKeyId == null) {
|
||||
throw new PassphraseCacheInterface.NoSecretKeyException();
|
||||
}
|
||||
return getCachedPassphrase(key.symmetric, key.symmetric);
|
||||
} catch (NotFoundException e) {
|
||||
throw new PassphraseCacheInterface.NoSecretKeyException();
|
||||
return getCachedPassphrase(masterKeyId, subKeyId);
|
||||
}
|
||||
return getCachedPassphrase(key.symmetric, key.symmetric);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
|
||||
abstract class BaseReadWriteOperation<T extends Parcelable> extends BaseOperation<T> {
|
||||
final KeyWritableRepository mKeyWritableRepository;
|
||||
public abstract class BaseReadWriteOperation<T extends Parcelable> extends BaseOperation<T> {
|
||||
protected final KeyWritableRepository mKeyWritableRepository;
|
||||
|
||||
BaseReadWriteOperation(Context context,
|
||||
KeyWritableRepository databaseInteractor,
|
||||
@@ -36,8 +36,8 @@ abstract class BaseReadWriteOperation<T extends Parcelable> extends BaseOperatio
|
||||
mKeyWritableRepository = databaseInteractor;
|
||||
}
|
||||
|
||||
BaseReadWriteOperation(Context context, KeyWritableRepository databaseInteractor,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
protected BaseReadWriteOperation(Context context, KeyWritableRepository databaseInteractor,
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, databaseInteractor, progressable, cancelled);
|
||||
|
||||
mKeyWritableRepository = databaseInteractor;
|
||||
|
||||
@@ -42,7 +42,7 @@ import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.SignEncryptParcel;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.BenchmarkInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
@@ -19,10 +19,10 @@ package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.operations.results.CertifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
@@ -38,10 +38,9 @@ import org.sufficientlysecure.keychain.pgp.PgpCertifyOperation.PgpCertifyResult;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.LastUpdateInteractor;
|
||||
import org.sufficientlysecure.keychain.daos.KeyMetadataDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
|
||||
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
|
||||
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
@@ -62,13 +61,13 @@ import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
* @see CertifyActionsParcel
|
||||
*/
|
||||
public class CertifyOperation extends BaseReadWriteOperation<CertifyActionsParcel> {
|
||||
private final LastUpdateInteractor lastUpdateInteractor;
|
||||
private final KeyMetadataDao keyMetadataDao;
|
||||
|
||||
public CertifyOperation(Context context, KeyWritableRepository databaseInteractor, Progressable progressable, AtomicBoolean
|
||||
public CertifyOperation(Context context, KeyWritableRepository keyWritableRepository, Progressable progressable, CancellationSignal
|
||||
cancelled) {
|
||||
super(context, databaseInteractor, progressable, cancelled);
|
||||
super(context, keyWritableRepository, progressable, cancelled);
|
||||
|
||||
this.lastUpdateInteractor = LastUpdateInteractor.create(context);
|
||||
this.keyMetadataDao = KeyMetadataDao.create(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -85,10 +84,8 @@ public class CertifyOperation extends BaseReadWriteOperation<CertifyActionsParce
|
||||
|
||||
log.add(LogType.MSG_CRT_MASTER_FETCH, 1);
|
||||
|
||||
CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(masterKeyId);
|
||||
Passphrase passphrase;
|
||||
|
||||
switch (cachedPublicKeyRing.getSecretKeyType(masterKeyId)) {
|
||||
switch (mKeyRepository.getSecretKeyType(masterKeyId)) {
|
||||
case PASSPHRASE:
|
||||
passphrase = cryptoInput.getPassphrase();
|
||||
if (passphrase == null) {
|
||||
@@ -234,7 +231,7 @@ public class CertifyOperation extends BaseReadWriteOperation<CertifyActionsParce
|
||||
log.add(uploadResult, 2);
|
||||
|
||||
if (uploadResult.success()) {
|
||||
lastUpdateInteractor.renewKeyLastUpdatedTime(certifiedKey.getMasterKeyId(), true);
|
||||
keyMetadataDao.renewKeyLastUpdatedTime(certifiedKey.getMasterKeyId(), true);
|
||||
|
||||
uploadOk += 1;
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
|
||||
@@ -23,14 +23,13 @@ import java.util.Collections;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.operations.results.DeleteResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.UpdateTrustResult;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.service.DeleteKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
|
||||
@@ -19,10 +19,10 @@ package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
|
||||
@@ -35,9 +35,9 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.LastUpdateInteractor;
|
||||
import org.sufficientlysecure.keychain.daos.KeyMetadataDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
|
||||
@@ -57,14 +57,14 @@ import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
*
|
||||
*/
|
||||
public class EditKeyOperation extends BaseReadWriteOperation<SaveKeyringParcel> {
|
||||
private final LastUpdateInteractor lastUpdateInteractor;
|
||||
private final KeyMetadataDao keyMetadataDao;
|
||||
|
||||
|
||||
public EditKeyOperation(Context context, KeyWritableRepository databaseInteractor,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, databaseInteractor, progressable, cancelled);
|
||||
|
||||
this.lastUpdateInteractor = LastUpdateInteractor.create(context);
|
||||
this.keyMetadataDao = KeyMetadataDao.create(context);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,8 +171,8 @@ public class EditKeyOperation extends BaseReadWriteOperation<SaveKeyringParcel>
|
||||
SaveKeyringResult saveResult = mKeyWritableRepository.saveSecretKeyRing(ring);
|
||||
log.add(saveResult, 1);
|
||||
|
||||
if (isNewKey) {
|
||||
lastUpdateInteractor.renewKeyLastUpdatedTime(ring.getMasterKeyId(), saveParcel.isShouldUpload());
|
||||
if (isNewKey || saveParcel.isShouldUpload()) {
|
||||
keyMetadataDao.renewKeyLastUpdatedTime(ring.getMasterKeyId(), saveParcel.isShouldUpload());
|
||||
}
|
||||
|
||||
// If the save operation didn't succeed, exit here
|
||||
|
||||
@@ -29,11 +29,11 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.keyimport.FacebookKeyserverClient;
|
||||
@@ -54,8 +54,8 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.LastUpdateInteractor;
|
||||
import org.sufficientlysecure.keychain.daos.KeyMetadataDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
@@ -90,7 +90,7 @@ public class ImportOperation extends BaseReadWriteOperation<ImportKeyringParcel>
|
||||
|
||||
public static final String CACHE_FILE_NAME = "key_import.pcl";
|
||||
|
||||
private final LastUpdateInteractor lastUpdateInteractor;
|
||||
private final KeyMetadataDao keyMetadataDao;
|
||||
|
||||
private FacebookKeyserverClient facebookServer;
|
||||
private KeybaseKeyserverClient keybaseServer;
|
||||
@@ -98,14 +98,14 @@ public class ImportOperation extends BaseReadWriteOperation<ImportKeyringParcel>
|
||||
public ImportOperation(Context context, KeyWritableRepository databaseInteractor, Progressable progressable) {
|
||||
super(context, databaseInteractor, progressable);
|
||||
|
||||
this.lastUpdateInteractor = LastUpdateInteractor.create(context);
|
||||
this.keyMetadataDao = KeyMetadataDao.create(context);
|
||||
}
|
||||
|
||||
public ImportOperation(Context context, KeyWritableRepository databaseInteractor,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, databaseInteractor, progressable, cancelled);
|
||||
|
||||
this.lastUpdateInteractor = LastUpdateInteractor.create(context);
|
||||
this.keyMetadataDao = KeyMetadataDao.create(context);
|
||||
}
|
||||
|
||||
// Overloaded functions for using progressable supplied in constructor during import
|
||||
@@ -200,7 +200,7 @@ public class ImportOperation extends BaseReadWriteOperation<ImportKeyringParcel>
|
||||
|
||||
byte[] fingerprintHex = entry.getExpectedFingerprint();
|
||||
if (fingerprintHex != null) {
|
||||
lastUpdateInteractor.renewKeyLastUpdatedTime(
|
||||
keyMetadataDao.renewKeyLastUpdatedTime(
|
||||
KeyFormattingUtils.getKeyIdFromFingerprint(fingerprintHex), false);
|
||||
}
|
||||
continue;
|
||||
@@ -249,8 +249,8 @@ public class ImportOperation extends BaseReadWriteOperation<ImportKeyringParcel>
|
||||
importedMasterKeyIds.add(key.getMasterKeyId());
|
||||
}
|
||||
|
||||
if (!skipSave) {
|
||||
lastUpdateInteractor.renewKeyLastUpdatedTime(key.getMasterKeyId(), keyWasDownloaded);
|
||||
if (!skipSave && keyWasDownloaded) {
|
||||
keyMetadataDao.renewKeyLastUpdatedTime(key.getMasterKeyId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.Operat
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
|
||||
import org.sufficientlysecure.keychain.service.InputDataParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.daos.KeyMetadataDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class KeySyncOperation extends BaseReadWriteOperation<KeySyncParcel> {
|
||||
// time since last update after which a key should be updated again, in s
|
||||
private static final long KEY_STALE_THRESHOLD_MILLIS =
|
||||
Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toMillis(7);
|
||||
// Time taken by Orbot before a new circuit is created
|
||||
private static final int ORBOT_CIRCUIT_TIMEOUT_SECONDS =
|
||||
Constants.DEBUG_KEYSERVER_SYNC ? 2 : (int) TimeUnit.MINUTES.toSeconds(10);
|
||||
|
||||
private final KeyMetadataDao keyMetadataDao;
|
||||
private final Preferences preferences;
|
||||
|
||||
public KeySyncOperation(Context context, KeyWritableRepository databaseInteractor,
|
||||
Progressable progressable, CancellationSignal cancellationSignal) {
|
||||
super(context, databaseInteractor, progressable, cancellationSignal);
|
||||
|
||||
keyMetadataDao = KeyMetadataDao.create(context);
|
||||
preferences = Preferences.getPreferences(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImportKeyResult execute(KeySyncParcel input, CryptoInputParcel cryptoInput) {
|
||||
long staleKeyThreshold = System.currentTimeMillis() - (input.getRefreshAll() ? 0 : KEY_STALE_THRESHOLD_MILLIS);
|
||||
List<byte[]> staleKeyFingerprints =
|
||||
keyMetadataDao.getFingerprintsForKeysOlderThan(staleKeyThreshold, TimeUnit.MILLISECONDS);
|
||||
List<ParcelableKeyRing> staleKeyParcelableKeyRings = fingerprintListToParcelableKeyRings(staleKeyFingerprints);
|
||||
|
||||
if (checkCancelled()) { // if we've already been cancelled
|
||||
return new ImportKeyResult(OperationResult.RESULT_CANCELLED, new OperationResult.OperationLog());
|
||||
}
|
||||
|
||||
// no explicit proxy, retrieve from preferences. Check if we should do a staggered sync
|
||||
CryptoInputParcel cryptoInputParcel = CryptoInputParcel.createCryptoInputParcel();
|
||||
|
||||
ImportKeyResult importKeyResult;
|
||||
if (preferences.getParcelableProxy().isTorEnabled()) {
|
||||
importKeyResult = staggeredUpdate(staleKeyParcelableKeyRings, cryptoInputParcel);
|
||||
} else {
|
||||
importKeyResult =
|
||||
directUpdate(staleKeyParcelableKeyRings, cryptoInputParcel);
|
||||
}
|
||||
return importKeyResult;
|
||||
}
|
||||
|
||||
private List<ParcelableKeyRing> fingerprintListToParcelableKeyRings(List<byte[]> staleKeyFingerprints) {
|
||||
ArrayList<ParcelableKeyRing> result = new ArrayList<>(staleKeyFingerprints.size());
|
||||
for (byte[] fingerprint : staleKeyFingerprints) {
|
||||
Timber.d("Keyserver sync: Updating %s", KeyFormattingUtils.beautifyKeyId(fingerprint));
|
||||
result.add(ParcelableKeyRing.createFromReference(fingerprint, null, null, null));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ImportKeyResult directUpdate(List<ParcelableKeyRing> keyList,
|
||||
CryptoInputParcel cryptoInputParcel) {
|
||||
Timber.d("Starting normal update");
|
||||
ImportOperation importOp = new ImportOperation(mContext, mKeyWritableRepository, mProgressable, mCancelled);
|
||||
return importOp.execute(
|
||||
ImportKeyringParcel.createImportKeyringParcel(keyList, preferences.getPreferredKeyserver()),
|
||||
cryptoInputParcel
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* will perform a staggered update of user's keys using delays to ensure new Tor circuits, as
|
||||
* performed by parcimonie. Relevant issue and method at:
|
||||
* https://github.com/open-keychain/open-keychain/issues/1337
|
||||
*
|
||||
* @return result of the sync
|
||||
*/
|
||||
private ImportKeyResult staggeredUpdate(List<ParcelableKeyRing> keyList,
|
||||
CryptoInputParcel cryptoInputParcel) {
|
||||
Timber.d("Starting staggered update");
|
||||
// final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
|
||||
// we are limiting our randomness to ORBOT_CIRCUIT_TIMEOUT_SECONDS for now
|
||||
final int WEEK_IN_SECONDS = 0;
|
||||
|
||||
ImportOperation.KeyImportAccumulator accumulator
|
||||
= new ImportOperation.KeyImportAccumulator(keyList.size(), null);
|
||||
|
||||
// so that the first key can be updated without waiting. This is so that there isn't a
|
||||
// large gap between a "Start Orbot" notification and the next key update
|
||||
boolean first = true;
|
||||
|
||||
for (ParcelableKeyRing keyRing : keyList) {
|
||||
int waitTime;
|
||||
int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size()));
|
||||
if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT_SECONDS) {
|
||||
waitTime = staggeredTime;
|
||||
} else {
|
||||
waitTime = ORBOT_CIRCUIT_TIMEOUT_SECONDS
|
||||
+ new Random().nextInt(1 + ORBOT_CIRCUIT_TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
if (first) {
|
||||
waitTime = 0;
|
||||
first = false;
|
||||
}
|
||||
|
||||
Timber.d("Updating key with a wait time of %d seconds", waitTime);
|
||||
try {
|
||||
Thread.sleep(waitTime * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
Timber.e(e, "Exception during sleep between key updates");
|
||||
// skip this one
|
||||
continue;
|
||||
}
|
||||
ArrayList<ParcelableKeyRing> keyWrapper = new ArrayList<>();
|
||||
keyWrapper.add(keyRing);
|
||||
if (checkCancelled()) {
|
||||
return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED,
|
||||
new OperationResult.OperationLog());
|
||||
}
|
||||
ImportKeyResult result =
|
||||
new ImportOperation(mContext, mKeyWritableRepository, null, mCancelled)
|
||||
.execute(
|
||||
ImportKeyringParcel.createImportKeyringParcel(
|
||||
keyWrapper,
|
||||
preferences.getPreferredKeyserver()
|
||||
),
|
||||
cryptoInputParcel
|
||||
);
|
||||
if (result.isPending()) {
|
||||
return result;
|
||||
}
|
||||
accumulator.accumulateKeyImport(result);
|
||||
}
|
||||
return accumulator.getConsolidatedResult();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
|
||||
|
||||
@AutoValue
|
||||
public abstract class KeySyncParcel implements Parcelable {
|
||||
public abstract boolean getRefreshAll();
|
||||
|
||||
public static KeySyncParcel createRefreshAll() {
|
||||
return new AutoValue_KeySyncParcel(true);
|
||||
}
|
||||
|
||||
public static KeySyncParcel createRefreshOutdated() {
|
||||
return new AutoValue_KeySyncParcel(false);
|
||||
}
|
||||
}
|
||||
@@ -18,16 +18,29 @@
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.textuality.keybase.lib.KeybaseQuery;
|
||||
import com.textuality.keybase.lib.Proof;
|
||||
import com.textuality.keybase.lib.prover.Prover;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import de.measite.minidns.Client;
|
||||
import de.measite.minidns.DNSMessage;
|
||||
import de.measite.minidns.Question;
|
||||
import de.measite.minidns.Record;
|
||||
import de.measite.minidns.record.Data;
|
||||
import de.measite.minidns.record.TXT;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.json.JSONObject;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.network.OkHttpKeybaseClient;
|
||||
import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
|
||||
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
@@ -35,28 +48,12 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.network.OkHttpKeybaseClient;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.measite.minidns.Client;
|
||||
import de.measite.minidns.DNSMessage;
|
||||
import de.measite.minidns.Question;
|
||||
import de.measite.minidns.Record;
|
||||
import de.measite.minidns.record.Data;
|
||||
import de.measite.minidns.record.TXT;
|
||||
|
||||
public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificationParcel> {
|
||||
|
||||
@@ -162,7 +159,7 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat
|
||||
}
|
||||
|
||||
long verifyingKeyId = decryptVerifyResult.getSignatureResult().getKeyId();
|
||||
byte[] verifyingFingerprint = mKeyRepository.getCachedPublicKeyRing(verifyingKeyId).getFingerprint();
|
||||
byte[] verifyingFingerprint = mKeyRepository.getFingerprintByKeyId(verifyingKeyId);
|
||||
if (!requiredFingerprint.equals(KeyFormattingUtils.convertFingerprintToHex(verifyingFingerprint))) {
|
||||
log.add(LogType.MSG_KEYBASE_ERROR_FINGERPRINT_MISMATCH, 1);
|
||||
return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log);
|
||||
|
||||
@@ -20,10 +20,10 @@ package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
@@ -35,8 +35,8 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.PromoteKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
@@ -50,7 +50,7 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
*/
|
||||
public class PromoteKeyOperation extends BaseReadWriteOperation<PromoteKeyringParcel> {
|
||||
public PromoteKeyOperation(Context context, KeyWritableRepository databaseInteractor,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, databaseInteractor, progressable, cancelled);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,17 +19,14 @@ package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
||||
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.RevokeResult;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.RevokeKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
@@ -58,19 +55,21 @@ public class RevokeOperation extends BaseReadWriteOperation<RevokeKeyringParcel>
|
||||
KeyFormattingUtils.beautifyKeyId(masterKeyId));
|
||||
|
||||
try {
|
||||
|
||||
Uri secretUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(masterKeyId);
|
||||
CachedPublicKeyRing keyRing = mKeyRepository.getCachedPublicKeyRing(secretUri);
|
||||
UnifiedKeyInfo keyInfo = mKeyRepository.getUnifiedKeyInfo(masterKeyId);
|
||||
if (keyInfo == null) {
|
||||
log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1);
|
||||
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
|
||||
}
|
||||
|
||||
// check if this is a master secret key we can work with
|
||||
switch (keyRing.getSecretKeyType(masterKeyId)) {
|
||||
switch (mKeyRepository.getSecretKeyType(masterKeyId)) {
|
||||
case GNU_DUMMY:
|
||||
log.add(OperationResult.LogType.MSG_EK_ERROR_DUMMY, 1);
|
||||
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
|
||||
}
|
||||
|
||||
SaveKeyringParcel.Builder saveKeyringParcel =
|
||||
SaveKeyringParcel.buildChangeKeyringParcel(masterKeyId, keyRing.getFingerprint());
|
||||
SaveKeyringParcel.buildChangeKeyringParcel(masterKeyId, keyInfo.fingerprint());
|
||||
|
||||
// all revoke operations are made atomic as of now
|
||||
saveKeyringParcel.setUpdateOptions(revokeKeyringParcel.isShouldUpload(), true,
|
||||
@@ -96,7 +95,7 @@ public class RevokeOperation extends BaseReadWriteOperation<RevokeKeyringParcel>
|
||||
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
|
||||
}
|
||||
|
||||
} catch (PgpKeyNotFoundException | KeyWritableRepository.NotFoundException e) {
|
||||
} catch (KeyWritableRepository.NotFoundException e) {
|
||||
Timber.e(e, "could not find key to revoke");
|
||||
log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1);
|
||||
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
|
||||
|
||||
@@ -20,22 +20,21 @@ package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.SignEncryptResult;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.SignEncryptParcel;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType;
|
||||
@@ -53,7 +52,7 @@ import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> {
|
||||
|
||||
public SignEncryptOperation(Context context, KeyRepository keyRepository,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, keyRepository, progressable, cancelled);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,15 +18,24 @@
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.daos.KeyMetadataDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
|
||||
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverClient;
|
||||
import org.sufficientlysecure.keychain.keyimport.KeyserverClient.AddKeyException;
|
||||
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
|
||||
import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.UploadResult;
|
||||
@@ -35,31 +44,26 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableProxy;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
|
||||
import timber.log.Timber;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
||||
/**
|
||||
* An operation class which implements the upload of a single key to a key server.
|
||||
*/
|
||||
public class UploadOperation extends BaseOperation<UploadKeyringParcel> {
|
||||
private KeyMetadataDao keyMetadataDao;
|
||||
|
||||
public UploadOperation(Context context, KeyRepository keyRepository,
|
||||
Progressable progressable, AtomicBoolean cancelled) {
|
||||
Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, keyRepository, progressable, cancelled);
|
||||
|
||||
keyMetadataDao = KeyMetadataDao.create(mContext);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -150,11 +154,18 @@ public class UploadOperation extends BaseOperation<UploadKeyringParcel> {
|
||||
keyring.encode(aos);
|
||||
aos.close();
|
||||
|
||||
if (checkCancelled()) {
|
||||
log.add(LogType.MSG_OPERATION_CANCELLED, 0);
|
||||
return new UploadResult(UploadResult.RESULT_CANCELLED, log);
|
||||
}
|
||||
|
||||
String armoredKey = bos.toString("UTF-8");
|
||||
keyserverInteractor.add(armoredKey, proxy);
|
||||
|
||||
updateProgress(R.string.progress_uploading, 1, 1);
|
||||
|
||||
keyMetadataDao.renewKeyLastUpdatedTime(keyring.getMasterKeyId(), true);
|
||||
|
||||
log.add(LogType.MSG_UPLOAD_SUCCESS, 1);
|
||||
return new UploadResult(UploadResult.RESULT_OK, log);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -419,7 +419,6 @@ public abstract class OperationResult implements Parcelable {
|
||||
// import secret
|
||||
MSG_IS(LogLevel.START, R.string.msg_is),
|
||||
MSG_IS_BAD_TYPE_PUBLIC (LogLevel.WARN, R.string.msg_is_bad_type_public),
|
||||
MSG_IS_DB_EXCEPTION (LogLevel.DEBUG, R.string.msg_is_db_exception),
|
||||
MSG_IS_ERROR_IO_EXC(LogLevel.DEBUG, R.string.msg_is_error_io_exc),
|
||||
MSG_IS_MERGE_PUBLIC (LogLevel.DEBUG, R.string.msg_is_merge_public),
|
||||
MSG_IS_MERGE_SECRET (LogLevel.DEBUG, R.string.msg_is_merge_secret),
|
||||
|
||||
@@ -43,9 +43,9 @@ import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
*/
|
||||
public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
|
||||
private final int mVerified;
|
||||
private final VerificationStatus mVerified;
|
||||
|
||||
CanonicalizedKeyRing(int verified) {
|
||||
CanonicalizedKeyRing(VerificationStatus verified) {
|
||||
mVerified = verified;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
return getRing().getPublicKey().getKeyID();
|
||||
}
|
||||
|
||||
public int getVerified() {
|
||||
public VerificationStatus getVerified() {
|
||||
return mVerified;
|
||||
}
|
||||
|
||||
@@ -61,15 +61,11 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
return getRing().getPublicKey().getFingerprint();
|
||||
}
|
||||
|
||||
public byte[] getRawPrimaryUserId() throws PgpKeyNotFoundException {
|
||||
public byte[] getRawPrimaryUserId() {
|
||||
return getPublicKey().getRawPrimaryUserId();
|
||||
}
|
||||
|
||||
public String getPrimaryUserId() throws PgpKeyNotFoundException {
|
||||
return getPublicKey().getPrimaryUserId();
|
||||
}
|
||||
|
||||
public String getPrimaryUserIdWithFallback() throws PgpKeyNotFoundException {
|
||||
public String getPrimaryUserIdWithFallback() {
|
||||
return getPublicKey().getPrimaryUserIdWithFallback();
|
||||
}
|
||||
|
||||
@@ -107,10 +103,6 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
return creationDate.after(now) || (expirationDate != null && expirationDate.before(now));
|
||||
}
|
||||
|
||||
public boolean canCertify() throws PgpKeyNotFoundException {
|
||||
return getRing().getPublicKey().isEncryptionKey();
|
||||
}
|
||||
|
||||
public Set<Long> getEncryptIds() {
|
||||
HashSet<Long> result = new HashSet<>();
|
||||
for (CanonicalizedPublicKey key : publicKeyIterator()) {
|
||||
@@ -130,15 +122,6 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
throw new PgpKeyNotFoundException("No valid encryption key found!");
|
||||
}
|
||||
|
||||
public boolean hasEncrypt() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
getEncryptId();
|
||||
return true;
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public long getSigningId() throws PgpKeyNotFoundException {
|
||||
for(CanonicalizedPublicKey key : publicKeyIterator()) {
|
||||
if (key.canSign() && key.isValid()) {
|
||||
@@ -194,4 +177,8 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
|
||||
return false;
|
||||
}
|
||||
|
||||
public enum VerificationStatus {
|
||||
UNVERIFIED, VERIFIED_SELF, VERIFIED_SECRET
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing {
|
||||
|
||||
private PGPPublicKeyRing mRing;
|
||||
|
||||
CanonicalizedPublicKeyRing(PGPPublicKeyRing ring, int verified) {
|
||||
CanonicalizedPublicKeyRing(PGPPublicKeyRing ring, VerificationStatus verified) {
|
||||
super(verified);
|
||||
mRing = ring;
|
||||
}
|
||||
|
||||
public CanonicalizedPublicKeyRing(byte[] blob, int verified) {
|
||||
public CanonicalizedPublicKeyRing(byte[] blob, VerificationStatus verified) {
|
||||
super(verified);
|
||||
if(mRing == null) {
|
||||
// get first object in block
|
||||
@@ -100,7 +100,7 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing {
|
||||
* - the user id that matches the userIdToKeep parameter, or the primary user id if none matches
|
||||
* each with their most recent binding certificates
|
||||
*/
|
||||
public CanonicalizedPublicKeyRing minimize(@Nullable String userIdToKeep) throws IOException, PgpKeyNotFoundException {
|
||||
public CanonicalizedPublicKeyRing minimize(@Nullable String userIdToKeep) throws IOException {
|
||||
CanonicalizedPublicKey masterKey = getPublicKey();
|
||||
PGPPublicKey masterPubKey = masterKey.getPublicKey();
|
||||
boolean userIdStrippedOk = false;
|
||||
|
||||
@@ -47,8 +47,7 @@ import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.SessionKeySecretKeyDecryptorBuilder;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import timber.log.Timber;
|
||||
@@ -326,7 +325,7 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey {
|
||||
spGen.setSignatureCreationTime(false, creationTimestamp);
|
||||
signatureGenerator.setHashedSubpackets(spGen.generate());
|
||||
return signatureGenerator;
|
||||
} catch (PgpKeyNotFoundException | PGPException e) {
|
||||
} catch (PGPException e) {
|
||||
// TODO: simply throw PGPException!
|
||||
throw new PgpGeneralException("Error initializing signature!", e);
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ public class CanonicalizedSecretKeyRing extends CanonicalizedKeyRing {
|
||||
|
||||
private PGPSecretKeyRing mRing;
|
||||
|
||||
CanonicalizedSecretKeyRing(PGPSecretKeyRing ring, int verified) {
|
||||
CanonicalizedSecretKeyRing(PGPSecretKeyRing ring, VerificationStatus verified) {
|
||||
super(verified);
|
||||
mRing = ring;
|
||||
}
|
||||
|
||||
public CanonicalizedSecretKeyRing(byte[] blob, int verified)
|
||||
public CanonicalizedSecretKeyRing(byte[] blob, VerificationStatus verified)
|
||||
{
|
||||
super(verified);
|
||||
JcaPGPObjectFactory factory = new JcaPGPObjectFactory(blob);
|
||||
|
||||
@@ -17,16 +17,12 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
import org.openintents.openpgp.util.OpenPgpUtils.UserId;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* An abstract KeyRing.
|
||||
* <p/>
|
||||
@@ -36,29 +32,18 @@ import java.util.regex.Pattern;
|
||||
* here.
|
||||
*
|
||||
* @see CanonicalizedKeyRing
|
||||
* @see org.sufficientlysecure.keychain.provider.CachedPublicKeyRing
|
||||
*/
|
||||
public abstract class KeyRing {
|
||||
|
||||
abstract public long getMasterKeyId() throws PgpKeyNotFoundException;
|
||||
|
||||
abstract public String getPrimaryUserId() throws PgpKeyNotFoundException;
|
||||
|
||||
abstract public String getPrimaryUserIdWithFallback() throws PgpKeyNotFoundException;
|
||||
|
||||
public UserId getSplitPrimaryUserIdWithFallback() throws PgpKeyNotFoundException {
|
||||
return splitUserId(getPrimaryUserIdWithFallback());
|
||||
}
|
||||
|
||||
abstract public boolean isRevoked() throws PgpKeyNotFoundException;
|
||||
|
||||
abstract public boolean canCertify() throws PgpKeyNotFoundException;
|
||||
|
||||
abstract public long getEncryptId() throws PgpKeyNotFoundException;
|
||||
|
||||
abstract public boolean hasEncrypt() throws PgpKeyNotFoundException;
|
||||
|
||||
abstract public int getVerified() throws PgpKeyNotFoundException;
|
||||
abstract public VerificationStatus getVerified() throws PgpKeyNotFoundException;
|
||||
|
||||
/**
|
||||
* Splits userId string into naming part, email part, and comment part
|
||||
|
||||
@@ -20,14 +20,14 @@ package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult.SenderStatusResult;
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
import org.openintents.openpgp.util.OpenPgpUtils.UserId;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ public class OpenPgpSignatureResultBuilder {
|
||||
|
||||
// OpenPgpSignatureResult
|
||||
private String mPrimaryUserId;
|
||||
private ArrayList<String> mUserIds = new ArrayList<>();
|
||||
private ArrayList<String> mConfirmedUserIds;
|
||||
private List<String> mUserIds = new ArrayList<>();
|
||||
private List<String> mConfirmedUserIds;
|
||||
private long mKeyId;
|
||||
private SenderStatusResult mSenderStatusResult;
|
||||
|
||||
@@ -101,7 +101,7 @@ public class OpenPgpSignatureResultBuilder {
|
||||
this.mIsKeyExpired = keyExpired;
|
||||
}
|
||||
|
||||
public void setUserIds(ArrayList<String> userIds, ArrayList<String> confirmedUserIds) {
|
||||
public void setUserIds(List<String> userIds, List<String> confirmedUserIds) {
|
||||
this.mUserIds = userIds;
|
||||
this.mConfirmedUserIds = confirmedUserIds;
|
||||
}
|
||||
@@ -118,20 +118,11 @@ public class OpenPgpSignatureResultBuilder {
|
||||
|
||||
// from RING
|
||||
setKeyId(signingRing.getMasterKeyId());
|
||||
try {
|
||||
setPrimaryUserId(signingRing.getPrimaryUserIdWithFallback());
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
Timber.d("No primary user id in keyring with master key id " + signingRing.getMasterKeyId());
|
||||
}
|
||||
setSignatureKeyCertified(signingRing.getVerified() > 0);
|
||||
setPrimaryUserId(signingRing.getPrimaryUserIdWithFallback());
|
||||
setSignatureKeyCertified(signingRing.getVerified() == VerificationStatus.VERIFIED_SECRET);
|
||||
|
||||
ArrayList<String> allUserIds = signingRing.getUnorderedUserIds();
|
||||
ArrayList<String> confirmedUserIds;
|
||||
try {
|
||||
confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId());
|
||||
} catch (NotFoundException e) {
|
||||
throw new IllegalStateException("Key didn't exist anymore for user id query!", e);
|
||||
}
|
||||
List<String> allUserIds = signingRing.getUnorderedUserIds();
|
||||
List<String> confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId());
|
||||
setUserIds(allUserIds, confirmedUserIds);
|
||||
|
||||
mSenderStatusResult = processSenderStatusResult(allUserIds, confirmedUserIds);
|
||||
@@ -142,7 +133,7 @@ public class OpenPgpSignatureResultBuilder {
|
||||
}
|
||||
|
||||
private SenderStatusResult processSenderStatusResult(
|
||||
ArrayList<String> allUserIds, ArrayList<String> confirmedUserIds) {
|
||||
List<String> allUserIds, List<String> confirmedUserIds) {
|
||||
if (mSenderAddress == null) {
|
||||
return SenderStatusResult.UNKNOWN;
|
||||
}
|
||||
@@ -156,7 +147,7 @@ public class OpenPgpSignatureResultBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean userIdListContainsAddress(String senderAddress, ArrayList<String> confirmedUserIds) {
|
||||
private static boolean userIdListContainsAddress(String senderAddress, List<String> confirmedUserIds) {
|
||||
for (String rawUserId : confirmedUserIds) {
|
||||
UserId userId = OpenPgpUtils.splitUserId(rawUserId);
|
||||
if (senderAddress.equalsIgnoreCase(userId.email)) {
|
||||
|
||||
@@ -71,11 +71,8 @@ import org.sufficientlysecure.keychain.pgp.SecurityProblem.EncryptionAlgorithmPr
|
||||
import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem;
|
||||
import org.sufficientlysecure.keychain.pgp.SecurityProblem.MissingMdc;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequireAnyDecryptPassphraseBuilder;
|
||||
@@ -635,13 +632,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
break;
|
||||
}
|
||||
|
||||
CachedPublicKeyRing cachedPublicKeyRing;
|
||||
try {
|
||||
// get actual keyring object based on master key id
|
||||
cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId)
|
||||
);
|
||||
long masterKeyId = cachedPublicKeyRing.getMasterKeyId();
|
||||
Long masterKeyId = mKeyRepository.getMasterKeyIdBySubkeyId(subKeyId);
|
||||
if (masterKeyId == null) {
|
||||
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// allow only specific keys for decryption?
|
||||
if (input.getAllowedKeyIds() != null) {
|
||||
@@ -658,7 +655,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
}
|
||||
}
|
||||
|
||||
SecretKeyType secretKeyType = cachedPublicKeyRing.getSecretKeyType(subKeyId);
|
||||
SecretKeyType secretKeyType = mKeyRepository.getSecretKeyType(subKeyId);
|
||||
if (!secretKeyType.isUsable()) {
|
||||
decryptionKey = null;
|
||||
log.add(LogType.MSG_DC_ASKIP_UNAVAILABLE, indent + 1);
|
||||
@@ -713,7 +710,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
encryptedDataAsymmetric = encData;
|
||||
decryptionKey = candidateDecryptionKey;
|
||||
|
||||
} catch (PgpKeyNotFoundException | KeyWritableRepository.NotFoundException e) {
|
||||
} catch (KeyWritableRepository.NotFoundException e) {
|
||||
// continue with the next packet in the while loop
|
||||
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
|
||||
continue;
|
||||
|
||||
@@ -33,7 +33,8 @@ import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.nist.NISTNamedCurves;
|
||||
@@ -108,7 +109,7 @@ import timber.log.Timber;
|
||||
public class PgpKeyOperation {
|
||||
|
||||
private Stack<Progressable> mProgress;
|
||||
private AtomicBoolean mCancelled;
|
||||
private CancellationSignal mCancelled;
|
||||
|
||||
public PgpKeyOperation(Progressable progress) {
|
||||
super();
|
||||
@@ -118,13 +119,13 @@ public class PgpKeyOperation {
|
||||
}
|
||||
}
|
||||
|
||||
public PgpKeyOperation(Progressable progress, AtomicBoolean cancelled) {
|
||||
public PgpKeyOperation(Progressable progress, CancellationSignal cancelled) {
|
||||
this(progress);
|
||||
mCancelled = cancelled;
|
||||
}
|
||||
|
||||
private boolean checkCancelled() {
|
||||
return mCancelled != null && mCancelled.get();
|
||||
return mCancelled != null && mCancelled.isCanceled();
|
||||
}
|
||||
|
||||
private void subProgressPush(int from, int to) {
|
||||
|
||||
@@ -33,11 +33,11 @@ import java.security.SignatureException;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.bcpg.BCPGOutputStream;
|
||||
@@ -63,10 +63,9 @@ import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainComp
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainHashAlgorithmTags;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
@@ -105,7 +104,7 @@ public class PgpSignEncryptOperation extends BaseOperation<PgpSignEncryptInputPa
|
||||
}
|
||||
}
|
||||
|
||||
public PgpSignEncryptOperation(Context context, KeyRepository keyRepository, Progressable progressable, AtomicBoolean cancelled) {
|
||||
public PgpSignEncryptOperation(Context context, KeyRepository keyRepository, Progressable progressable, CancellationSignal cancelled) {
|
||||
super(context, keyRepository, progressable, cancelled);
|
||||
}
|
||||
|
||||
@@ -226,8 +225,8 @@ public class PgpSignEncryptOperation extends BaseOperation<PgpSignEncryptInputPa
|
||||
Long signingSubKeyId = data.getSignatureSubKeyId();
|
||||
if (signingSubKeyId == null) {
|
||||
try {
|
||||
signingSubKeyId = mKeyRepository.getCachedPublicKeyRing(signingMasterKeyId).getSecretSignId();
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
signingSubKeyId = mKeyRepository.getSecretSignId(signingMasterKeyId);
|
||||
} catch (NotFoundException e) {
|
||||
log.add(LogType.MSG_PSE_ERROR_KEY_SIGN, indent);
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
@@ -257,7 +256,7 @@ public class PgpSignEncryptOperation extends BaseOperation<PgpSignEncryptInputPa
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
switch (mKeyRepository.getCachedPublicKeyRing(signingMasterKeyId).getSecretKeyType(signingSubKeyId)) {
|
||||
switch (mKeyRepository.getSecretKeyType(signingSubKeyId)) {
|
||||
case DIVERT_TO_CARD:
|
||||
case PASSPHRASE_EMPTY: {
|
||||
if (!signingKey.unlock(new Passphrase())) {
|
||||
@@ -651,8 +650,7 @@ public class PgpSignEncryptOperation extends BaseOperation<PgpSignEncryptInputPa
|
||||
private boolean processEncryptionMasterKeyId(int indent, OperationLog log, PgpSignEncryptData data,
|
||||
PGPEncryptedDataGenerator cPk, long encryptMasterKeyId) {
|
||||
try {
|
||||
CanonicalizedPublicKeyRing keyRing = mKeyRepository.getCanonicalizedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingUri(encryptMasterKeyId));
|
||||
CanonicalizedPublicKeyRing keyRing = mKeyRepository.getCanonicalizedPublicKeyRing(encryptMasterKeyId);
|
||||
Set<Long> encryptSubKeyIds = keyRing.getEncryptIds();
|
||||
for (Long subKeyId : encryptSubKeyIds) {
|
||||
CanonicalizedPublicKey key = keyRing.getPublicKey(subKeyId);
|
||||
|
||||
@@ -38,9 +38,8 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.Operat
|
||||
import org.sufficientlysecure.keychain.pgp.DecryptVerifySecurityProblem.DecryptVerifySecurityProblemBuilder;
|
||||
import org.sufficientlysecure.keychain.pgp.SecurityProblem.InsecureSigningAlgorithm;
|
||||
import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
@@ -160,9 +159,11 @@ class PgpSignatureChecker {
|
||||
for (int i = 0; i < sigList.size(); ++i) {
|
||||
try {
|
||||
long sigKeyId = sigList.get(i).getKeyID();
|
||||
CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
|
||||
);
|
||||
Long masterKeyId = mKeyRepository.getMasterKeyIdBySubkeyId(sigKeyId);
|
||||
if (masterKeyId == null) {
|
||||
continue;
|
||||
}
|
||||
CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing(masterKeyId);
|
||||
CanonicalizedPublicKey keyCandidate = signingRing.getPublicKey(sigKeyId);
|
||||
if ( ! keyCandidate.canSign()) {
|
||||
continue;
|
||||
@@ -183,9 +184,11 @@ class PgpSignatureChecker {
|
||||
for (int i = 0; i < sigList.size(); ++i) {
|
||||
try {
|
||||
long sigKeyId = sigList.get(i).getKeyID();
|
||||
CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
|
||||
);
|
||||
Long masterKeyId = mKeyRepository.getMasterKeyIdBySubkeyId(sigKeyId);
|
||||
if (masterKeyId == null) {
|
||||
continue;
|
||||
}
|
||||
CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing(masterKeyId);
|
||||
CanonicalizedPublicKey keyCandidate = signingRing.getPublicKey(sigKeyId);
|
||||
if ( ! keyCandidate.canSign()) {
|
||||
continue;
|
||||
|
||||
@@ -64,6 +64,7 @@ import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
@@ -1115,8 +1116,8 @@ public class UncachedKeyRing {
|
||||
log.add(LogType.MSG_KC_SUCCESS, indent);
|
||||
}
|
||||
|
||||
return isSecret() ? new CanonicalizedSecretKeyRing((PGPSecretKeyRing) ring, 1)
|
||||
: new CanonicalizedPublicKeyRing((PGPPublicKeyRing) ring, 0);
|
||||
return isSecret() ? new CanonicalizedSecretKeyRing((PGPSecretKeyRing) ring, VerificationStatus.VERIFIED_SECRET)
|
||||
: new CanonicalizedPublicKeyRing((PGPPublicKeyRing) ring, VerificationStatus.UNVERIFIED);
|
||||
}
|
||||
|
||||
/** This operation merges information from a different keyring, returning a combined
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
|
||||
|
||||
public class ApiDataAccessObject {
|
||||
|
||||
private final SimpleContentResolverInterface mQueryInterface;
|
||||
|
||||
public ApiDataAccessObject(Context context) {
|
||||
final ContentResolver contentResolver = context.getContentResolver();
|
||||
mQueryInterface = new SimpleContentResolverInterface() {
|
||||
@Override
|
||||
public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri contentUri, ContentValues values) {
|
||||
return contentResolver.insert(contentUri, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) {
|
||||
return contentResolver.update(contentUri, values, where, selectionArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri contentUri, String where, String[] selectionArgs) {
|
||||
return contentResolver.delete(contentUri, where, selectionArgs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public ApiDataAccessObject(SimpleContentResolverInterface queryInterface) {
|
||||
mQueryInterface = queryInterface;
|
||||
}
|
||||
|
||||
public ArrayList<String> getRegisteredApiApps() {
|
||||
Cursor cursor = mQueryInterface.query(ApiApps.CONTENT_URI, null, null, null, null);
|
||||
|
||||
ArrayList<String> packageNames = new ArrayList<>();
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
packageNames.add(cursor.getString(packageNameCol));
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return packageNames;
|
||||
}
|
||||
|
||||
private ContentValues contentValueForApiApps(AppSettings appSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
|
||||
values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageCertificate());
|
||||
return values;
|
||||
}
|
||||
|
||||
public void insertApiApp(AppSettings appSettings) {
|
||||
mQueryInterface.insert(ApiApps.CONTENT_URI, contentValueForApiApps(appSettings));
|
||||
}
|
||||
|
||||
public void deleteApiApp(String packageName) {
|
||||
mQueryInterface.delete(ApiApps.buildByPackageNameUri(packageName), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be an uri pointing to an account
|
||||
*/
|
||||
public AppSettings getApiAppSettings(Uri uri) {
|
||||
AppSettings settings = null;
|
||||
|
||||
Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
settings = new AppSettings();
|
||||
settings.setPackageName(cursor.getString(
|
||||
cursor.getColumnIndex(ApiApps.PACKAGE_NAME)));
|
||||
settings.setPackageCertificate(cursor.getBlob(
|
||||
cursor.getColumnIndex(ApiApps.PACKAGE_CERTIFICATE)));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public HashSet<Long> getAllowedKeyIdsForApp(Uri uri) {
|
||||
HashSet<Long> keyIds = new HashSet<>();
|
||||
|
||||
Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int keyIdColumn = cursor.getColumnIndex(ApiAllowedKeys.KEY_ID);
|
||||
while (cursor.moveToNext()) {
|
||||
keyIds.add(cursor.getLong(keyIdColumn));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public void saveAllowedKeyIdsForApp(Uri uri, Set<Long> allowedKeyIds)
|
||||
throws RemoteException, OperationApplicationException {
|
||||
// wipe whole table of allowed keys for this account
|
||||
mQueryInterface.delete(uri, null, null);
|
||||
|
||||
// re-insert allowed key ids
|
||||
for (Long keyId : allowedKeyIds) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiAllowedKeys.KEY_ID, keyId);
|
||||
mQueryInterface.insert(uri, values);
|
||||
}
|
||||
}
|
||||
|
||||
public void addAllowedKeyIdForApp(Uri uri, long allowedKeyId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiAllowedKeys.KEY_ID, allowedKeyId);
|
||||
mQueryInterface.insert(uri, values);
|
||||
}
|
||||
|
||||
public void addAllowedKeyIdForApp(String packageName, long allowedKeyId) {
|
||||
Uri uri = ApiAllowedKeys.buildBaseUri(packageName);
|
||||
addAllowedKeyIdForApp(uri, allowedKeyId);
|
||||
}
|
||||
|
||||
public byte[] getApiAppCertificate(String packageName) {
|
||||
Uri queryUri = ApiApps.buildByPackageNameUri(packageName);
|
||||
|
||||
String[] projection = new String[]{ApiApps.PACKAGE_CERTIFICATE};
|
||||
|
||||
Cursor cursor = mQueryInterface.query(queryUri, projection, null, null, null);
|
||||
try {
|
||||
byte[] signature = null;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int signatureCol = 0;
|
||||
|
||||
signature = cursor.getBlob(signatureCol);
|
||||
}
|
||||
return signature;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
|
||||
|
||||
public class AutocryptPeerDataAccessObject {
|
||||
private static final long AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS = 35 * DateUtils.DAY_IN_MILLIS;
|
||||
|
||||
private static final String[] PROJECTION_LAST_SEEN = { ApiAutocryptPeer.LAST_SEEN };
|
||||
private static final String[] PROJECTION_LAST_SEEN_KEY = { ApiAutocryptPeer.LAST_SEEN_KEY };
|
||||
private static final String[] PROJECTION_GOSSIP_LAST_SEEN_KEY = { ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY };
|
||||
private static final String[] PROJECTION_MASTER_KEY_ID = { ApiAutocryptPeer.MASTER_KEY_ID };
|
||||
|
||||
private static final String[] PROJECTION_AUTOCRYPT_QUERY = {
|
||||
ApiAutocryptPeer.IDENTIFIER,
|
||||
ApiAutocryptPeer.LAST_SEEN,
|
||||
ApiAutocryptPeer.MASTER_KEY_ID,
|
||||
ApiAutocryptPeer.LAST_SEEN_KEY,
|
||||
ApiAutocryptPeer.IS_MUTUAL,
|
||||
ApiAutocryptPeer.KEY_IS_REVOKED,
|
||||
ApiAutocryptPeer.KEY_IS_EXPIRED,
|
||||
ApiAutocryptPeer.KEY_IS_VERIFIED,
|
||||
ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID,
|
||||
ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY,
|
||||
ApiAutocryptPeer.GOSSIP_KEY_IS_REVOKED,
|
||||
ApiAutocryptPeer.GOSSIP_KEY_IS_EXPIRED,
|
||||
ApiAutocryptPeer.GOSSIP_KEY_IS_VERIFIED,
|
||||
};
|
||||
private static final int INDEX_IDENTIFIER = 0;
|
||||
private static final int INDEX_LAST_SEEN = 1;
|
||||
private static final int INDEX_MASTER_KEY_ID = 2;
|
||||
private static final int INDEX_LAST_SEEN_KEY = 3;
|
||||
private static final int INDEX_STATE = 4;
|
||||
private static final int INDEX_KEY_IS_REVOKED = 5;
|
||||
private static final int INDEX_KEY_IS_EXPIRED = 6;
|
||||
private static final int INDEX_KEY_IS_VERIFIED = 7;
|
||||
private static final int INDEX_GOSSIP_MASTER_KEY_ID = 8;
|
||||
private static final int INDEX_GOSSIP_LAST_SEEN_KEY = 9;
|
||||
private static final int INDEX_GOSSIP_KEY_IS_REVOKED = 10;
|
||||
private static final int INDEX_GOSSIP_KEY_IS_EXPIRED = 11;
|
||||
private static final int INDEX_GOSSIP_KEY_IS_VERIFIED = 12;
|
||||
|
||||
private final SimpleContentResolverInterface queryInterface;
|
||||
private final String packageName;
|
||||
private final DatabaseNotifyManager databaseNotifyManager;
|
||||
|
||||
public AutocryptPeerDataAccessObject(Context context, String packageName) {
|
||||
this.packageName = packageName;
|
||||
this.databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
|
||||
final ContentResolver contentResolver = context.getContentResolver();
|
||||
queryInterface = new SimpleContentResolverInterface() {
|
||||
@Override
|
||||
public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri contentUri, ContentValues values) {
|
||||
return contentResolver.insert(contentUri, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) {
|
||||
return contentResolver.update(contentUri, values, where, selectionArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri contentUri, String where, String[] selectionArgs) {
|
||||
return contentResolver.delete(contentUri, where, selectionArgs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public AutocryptPeerDataAccessObject(SimpleContentResolverInterface queryInterface, String packageName,
|
||||
DatabaseNotifyManager databaseNotifyManager) {
|
||||
this.queryInterface = queryInterface;
|
||||
this.packageName = packageName;
|
||||
this.databaseNotifyManager = databaseNotifyManager;
|
||||
}
|
||||
|
||||
public Long getMasterKeyIdForAutocryptPeer(String autocryptId) {
|
||||
Cursor cursor = queryInterface.query(
|
||||
ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId),
|
||||
PROJECTION_MASTER_KEY_ID, null, null, null);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(0);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Date getLastSeen(String autocryptId) {
|
||||
Cursor cursor = queryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId),
|
||||
PROJECTION_LAST_SEEN, null, null, null);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
long lastUpdated = cursor.getLong(0);
|
||||
return new Date(lastUpdated);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Date getLastSeenKey(String autocryptId) {
|
||||
Cursor cursor = queryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId),
|
||||
PROJECTION_LAST_SEEN_KEY, null, null, null);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
long lastUpdated = cursor.getLong(0);
|
||||
return new Date(lastUpdated);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Date getLastSeenGossip(String autocryptId) {
|
||||
Cursor cursor = queryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId),
|
||||
PROJECTION_GOSSIP_LAST_SEEN_KEY, null, null, null);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
long lastUpdated = cursor.getLong(0);
|
||||
return new Date(lastUpdated);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updateLastSeen(String autocryptId, Date date) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(ApiAutocryptPeer.LAST_SEEN, date.getTime());
|
||||
queryInterface
|
||||
.update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null);
|
||||
}
|
||||
|
||||
public void updateKey(String autocryptId, Date effectiveDate, long masterKeyId, boolean isMutual) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId);
|
||||
cv.put(ApiAutocryptPeer.LAST_SEEN_KEY, effectiveDate.getTime());
|
||||
cv.put(ApiAutocryptPeer.IS_MUTUAL, isMutual ? 1 : 0);
|
||||
queryInterface
|
||||
.update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null);
|
||||
databaseNotifyManager.notifyAutocryptUpdate(autocryptId, masterKeyId);
|
||||
}
|
||||
|
||||
public void updateKeyGossipFromAutocrypt(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
updateKeyGossip(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP_ORIGIN_AUTOCRYPT);
|
||||
}
|
||||
|
||||
public void updateKeyGossipFromSignature(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
updateKeyGossip(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP_ORIGIN_SIGNATURE);
|
||||
}
|
||||
|
||||
public void updateKeyGossipFromDedup(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
updateKeyGossip(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP_ORIGIN_DEDUP);
|
||||
}
|
||||
|
||||
private void updateKeyGossip(String autocryptId, Date effectiveDate, long masterKeyId, int origin) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID, masterKeyId);
|
||||
cv.put(ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY, effectiveDate.getTime());
|
||||
cv.put(ApiAutocryptPeer.GOSSIP_ORIGIN, origin);
|
||||
queryInterface
|
||||
.update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null);
|
||||
databaseNotifyManager.notifyAutocryptUpdate(autocryptId, masterKeyId);
|
||||
}
|
||||
|
||||
public void delete(String autocryptId) {
|
||||
Long masterKeyId = getMasterKeyIdForAutocryptPeer(autocryptId);
|
||||
queryInterface.delete(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), null, null);
|
||||
databaseNotifyManager.notifyAutocryptDelete(autocryptId, masterKeyId);
|
||||
}
|
||||
|
||||
public List<AutocryptRecommendationResult> determineAutocryptRecommendations(String... autocryptIds) {
|
||||
List<AutocryptRecommendationResult> result = new ArrayList<>(autocryptIds.length);
|
||||
|
||||
Cursor cursor = queryAutocryptPeerData(autocryptIds);
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(cursor);
|
||||
result.add(peerResult);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Determines Autocrypt "ui-recommendation", according to spec.
|
||||
* See https://autocrypt.org/level1.html#recommendations-for-single-recipient-messages
|
||||
*/
|
||||
private AutocryptRecommendationResult determineAutocryptRecommendation(Cursor cursor) {
|
||||
String peerId = cursor.getString(INDEX_IDENTIFIER);
|
||||
|
||||
AutocryptRecommendationResult keyRecommendation = determineAutocryptKeyRecommendation(peerId, cursor);
|
||||
if (keyRecommendation != null) return keyRecommendation;
|
||||
|
||||
AutocryptRecommendationResult gossipRecommendation = determineAutocryptGossipRecommendation(peerId, cursor);
|
||||
if (gossipRecommendation != null) return gossipRecommendation;
|
||||
|
||||
return new AutocryptRecommendationResult(peerId, AutocryptState.DISABLE, null, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AutocryptRecommendationResult determineAutocryptKeyRecommendation(String peerId, Cursor cursor) {
|
||||
boolean hasKey = !cursor.isNull(INDEX_MASTER_KEY_ID);
|
||||
boolean isRevoked = cursor.getInt(INDEX_KEY_IS_REVOKED) != 0;
|
||||
boolean isExpired = cursor.getInt(INDEX_KEY_IS_EXPIRED) != 0;
|
||||
if (!hasKey || isRevoked || isExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
long lastSeen = cursor.getLong(INDEX_LAST_SEEN);
|
||||
long lastSeenKey = cursor.getLong(INDEX_LAST_SEEN_KEY);
|
||||
boolean isVerified = cursor.getInt(INDEX_KEY_IS_VERIFIED) != 0;
|
||||
if (lastSeenKey < (lastSeen - AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS)) {
|
||||
return new AutocryptRecommendationResult(peerId, AutocryptState.DISCOURAGED_OLD, masterKeyId, isVerified);
|
||||
}
|
||||
|
||||
boolean isMutual = cursor.getInt(INDEX_STATE) != 0;
|
||||
if (isMutual) {
|
||||
return new AutocryptRecommendationResult(peerId, AutocryptState.MUTUAL, masterKeyId, isVerified);
|
||||
} else {
|
||||
return new AutocryptRecommendationResult(peerId, AutocryptState.AVAILABLE, masterKeyId, isVerified);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AutocryptRecommendationResult determineAutocryptGossipRecommendation(String peerId, Cursor cursor) {
|
||||
boolean gossipHasKey = !cursor.isNull(INDEX_GOSSIP_MASTER_KEY_ID);
|
||||
boolean gossipIsRevoked = cursor.getInt(INDEX_GOSSIP_KEY_IS_REVOKED) != 0;
|
||||
boolean gossipIsExpired = cursor.getInt(INDEX_GOSSIP_KEY_IS_EXPIRED) != 0;
|
||||
boolean isVerified = cursor.getInt(INDEX_GOSSIP_KEY_IS_VERIFIED) != 0;
|
||||
|
||||
if (!gossipHasKey || gossipIsRevoked || gossipIsExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long masterKeyId = cursor.getLong(INDEX_GOSSIP_MASTER_KEY_ID);
|
||||
return new AutocryptRecommendationResult(peerId, AutocryptState.DISCOURAGED_GOSSIP, masterKeyId, isVerified);
|
||||
}
|
||||
|
||||
private Cursor queryAutocryptPeerData(String[] autocryptIds) {
|
||||
StringBuilder selection = new StringBuilder(ApiAutocryptPeer.IDENTIFIER + " IN (?");
|
||||
for (int i = 1; i < autocryptIds.length; i++) {
|
||||
selection.append(",?");
|
||||
}
|
||||
selection.append(")");
|
||||
|
||||
return queryInterface.query(ApiAutocryptPeer.buildByPackageName(packageName),
|
||||
PROJECTION_AUTOCRYPT_QUERY, selection.toString(), autocryptIds, null);
|
||||
}
|
||||
|
||||
public static class AutocryptRecommendationResult {
|
||||
public final String peerId;
|
||||
public final Long masterKeyId;
|
||||
public final AutocryptState autocryptState;
|
||||
public final boolean isVerified;
|
||||
|
||||
AutocryptRecommendationResult(String peerId, AutocryptState autocryptState, Long masterKeyId,
|
||||
boolean isVerified) {
|
||||
this.peerId = peerId;
|
||||
this.autocryptState = autocryptState;
|
||||
this.masterKeyId = masterKeyId;
|
||||
this.isVerified = isVerified;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum AutocryptState {
|
||||
DISABLE, DISCOURAGED_OLD, DISCOURAGED_GOSSIP, AVAILABLE, MUTUAL
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.net.Uri;
|
||||
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
import org.sufficientlysecure.keychain.pgp.KeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
/** This implementation of KeyRing provides a cached view of PublicKeyRing
|
||||
* objects based on database queries exclusively.
|
||||
*
|
||||
* This class should be used where only few points of data but no actual
|
||||
* cryptographic operations are required about a PublicKeyRing which is already
|
||||
* in the database. This happens commonly in UI code, where parsing of a PGP
|
||||
* key for examination would be a very expensive operation.
|
||||
*
|
||||
* Each getter method is implemented using a more or less expensive database
|
||||
* query, while object construction is (almost) free. A common pattern is
|
||||
* mProviderHelper.getCachedKeyRing(uri).getterMethod()
|
||||
*
|
||||
* TODO Ensure that the values returned here always match the ones returned by
|
||||
* the parsed KeyRing!
|
||||
*
|
||||
*/
|
||||
public class CachedPublicKeyRing extends KeyRing {
|
||||
|
||||
final KeyRepository mKeyRepository;
|
||||
final Uri mUri;
|
||||
|
||||
public CachedPublicKeyRing(KeyRepository keyRepository, Uri uri) {
|
||||
mKeyRepository = keyRepository;
|
||||
mUri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMasterKeyId() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.MASTER_KEY_ID, KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (Long) data;
|
||||
} catch (KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the master key id related to a given query. The id will either be extracted from the
|
||||
* query, which should work for all specific /key_rings/ queries, or will be queried if it can't.
|
||||
*/
|
||||
public long extractOrGetMasterKeyId() throws PgpKeyNotFoundException {
|
||||
// try extracting from the uri first
|
||||
String firstSegment = mUri.getPathSegments().get(1);
|
||||
if (!"find".equals(firstSegment)) try {
|
||||
return Long.parseLong(firstSegment);
|
||||
} catch (NumberFormatException e) {
|
||||
// didn't work? oh well.
|
||||
Timber.d("Couldn't get masterKeyId from URI, querying...");
|
||||
}
|
||||
return getMasterKeyId();
|
||||
}
|
||||
|
||||
public byte[] getFingerprint() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.FINGERPRINT, KeyRepository.FIELD_TYPE_BLOB);
|
||||
return (byte[]) data;
|
||||
} catch (KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public long getCreationTime() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.CREATION, KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (long) data;
|
||||
} catch (KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrimaryUserId() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.USER_ID,
|
||||
KeyRepository.FIELD_TYPE_STRING);
|
||||
return (String) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getPrimaryUserIdWithFallback() throws PgpKeyNotFoundException {
|
||||
return getPrimaryUserId();
|
||||
}
|
||||
|
||||
public String getName() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeyRings.NAME,
|
||||
KeyRepository.FIELD_TYPE_STRING);
|
||||
return (String) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getEmail() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeyRings.EMAIL,
|
||||
KeyRepository.FIELD_TYPE_STRING);
|
||||
return (String) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getComment() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeyRings.COMMENT,
|
||||
KeyRepository.FIELD_TYPE_STRING);
|
||||
return (String) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRevoked() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.IS_REVOKED,
|
||||
KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (Long) data > 0;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCertify() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.HAS_CERTIFY_SECRET,
|
||||
KeyRepository.FIELD_TYPE_NULL);
|
||||
return !((Boolean) data);
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEncryptId() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeyRings.HAS_ENCRYPT,
|
||||
KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (Long) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasEncrypt() throws PgpKeyNotFoundException {
|
||||
return getEncryptId() != 0;
|
||||
}
|
||||
|
||||
/** Returns the key id which should be used for signing.
|
||||
*
|
||||
* This method returns keys which are actually available (ie. secret available, and not stripped,
|
||||
* revoked, or expired), hence only works on keyrings where a secret key is available!
|
||||
*
|
||||
*/
|
||||
public long getSecretSignId() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeyRings.HAS_SIGN_SECRET,
|
||||
KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (Long) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the key id which should be used for authentication.
|
||||
*
|
||||
* This method returns keys which are actually available (ie. secret available, and not stripped,
|
||||
* revoked, or expired), hence only works on keyrings where a secret key is available!
|
||||
*
|
||||
*/
|
||||
public long getSecretAuthenticationId() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeyRings.HAS_AUTHENTICATE_SECRET,
|
||||
KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (Long) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSecretAuthentication() throws PgpKeyNotFoundException {
|
||||
return getSecretAuthenticationId() != 0;
|
||||
}
|
||||
|
||||
public long getAuthenticationId() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeyRings.HAS_AUTHENTICATE,
|
||||
KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (Long) data;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAuthentication() throws PgpKeyNotFoundException {
|
||||
return getAuthenticationId() != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVerified() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.VERIFIED,
|
||||
KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return ((Long) data).intValue();
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAnySecret() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
Object data = mKeyRepository.getGenericData(mUri,
|
||||
KeychainContract.KeyRings.HAS_ANY_SECRET,
|
||||
KeyRepository.FIELD_TYPE_INTEGER);
|
||||
return (Long) data > 0;
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SecretKeyType getSecretKeyType(long keyId) throws NotFoundException {
|
||||
Object data = mKeyRepository.getGenericData(Keys.buildKeysUri(mUri),
|
||||
KeyRings.HAS_SECRET,
|
||||
KeyRepository.FIELD_TYPE_INTEGER,
|
||||
KeyRings.KEY_ID + " = " + Long.toString(keyId));
|
||||
return SecretKeyType.fromNum(((Long) data).intValue());
|
||||
}
|
||||
|
||||
public byte[] getEncoded() throws PgpKeyNotFoundException {
|
||||
try {
|
||||
return mKeyRepository.loadPublicKeyRingData(getMasterKeyId());
|
||||
} catch(KeyWritableRepository.NotFoundException e) {
|
||||
throw new PgpKeyNotFoundException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
|
||||
|
||||
public class DatabaseNotifyManager {
|
||||
private ContentResolver contentResolver;
|
||||
|
||||
public static DatabaseNotifyManager create(Context context) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
return new DatabaseNotifyManager(contentResolver);
|
||||
}
|
||||
|
||||
private DatabaseNotifyManager(ContentResolver contentResolver) {
|
||||
this.contentResolver = contentResolver;
|
||||
}
|
||||
|
||||
public void notifyKeyChange(long masterKeyId) {
|
||||
Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public void notifyAutocryptDelete(String autocryptId, Long masterKeyId) {
|
||||
Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public void notifyAutocryptUpdate(String autocryptId, long masterKeyId) {
|
||||
Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public void notifyKeyserverStatusChange(long masterKeyId) {
|
||||
Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId);
|
||||
contentResolver.notifyChange(uri, null);
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.HashMap;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class KeyRepository {
|
||||
// If we ever switch to api level 11, we can ditch this whole mess!
|
||||
public static final int FIELD_TYPE_NULL = 1;
|
||||
// this is called integer to stay coherent with the constants in Cursor (api level 11)
|
||||
public static final int FIELD_TYPE_INTEGER = 2;
|
||||
public static final int FIELD_TYPE_FLOAT = 3;
|
||||
public static final int FIELD_TYPE_STRING = 4;
|
||||
public static final int FIELD_TYPE_BLOB = 5;
|
||||
|
||||
final ContentResolver contentResolver;
|
||||
final LocalPublicKeyStorage mLocalPublicKeyStorage;
|
||||
OperationLog mLog;
|
||||
int mIndent;
|
||||
|
||||
public static KeyRepository create(Context context) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
LocalPublicKeyStorage localPublicKeyStorage = LocalPublicKeyStorage.getInstance(context);
|
||||
|
||||
return new KeyRepository(contentResolver, localPublicKeyStorage);
|
||||
}
|
||||
|
||||
private KeyRepository(ContentResolver contentResolver, LocalPublicKeyStorage localPublicKeyStorage) {
|
||||
this(contentResolver, localPublicKeyStorage, new OperationLog(), 0);
|
||||
}
|
||||
|
||||
KeyRepository(ContentResolver contentResolver, LocalPublicKeyStorage localPublicKeyStorage,
|
||||
OperationLog log, int indent) {
|
||||
this.contentResolver = contentResolver;
|
||||
mLocalPublicKeyStorage = localPublicKeyStorage;
|
||||
mIndent = indent;
|
||||
mLog = log;
|
||||
}
|
||||
|
||||
public OperationLog getLog() {
|
||||
return mLog;
|
||||
}
|
||||
|
||||
public void log(LogType type) {
|
||||
if (mLog != null) {
|
||||
mLog.add(type, mIndent);
|
||||
}
|
||||
}
|
||||
|
||||
public void log(LogType type, Object... parameters) {
|
||||
if (mLog != null) {
|
||||
mLog.add(type, mIndent, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearLog() {
|
||||
mLog = new OperationLog();
|
||||
}
|
||||
|
||||
Object getGenericData(Uri uri, String column, int type) throws NotFoundException {
|
||||
Object result = getGenericData(uri, new String[]{column}, new int[]{type}, null).get(column);
|
||||
if (result == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Object getGenericDataOrNull(Uri uri, String column, int type) throws NotFoundException {
|
||||
return getGenericData(uri, new String[]{column}, new int[]{type}, null).get(column);
|
||||
}
|
||||
|
||||
Object getGenericData(Uri uri, String column, int type, String selection)
|
||||
throws NotFoundException {
|
||||
return getGenericData(uri, new String[]{column}, new int[]{type}, selection).get(column);
|
||||
}
|
||||
|
||||
private HashMap<String, Object> getGenericData(Uri uri, String[] proj, int[] types)
|
||||
throws NotFoundException {
|
||||
return getGenericData(uri, proj, types, null);
|
||||
}
|
||||
|
||||
private HashMap<String, Object> getGenericData(Uri uri, String[] proj, int[] types, String selection)
|
||||
throws NotFoundException {
|
||||
Cursor cursor = contentResolver.query(uri, proj, selection, null, null);
|
||||
|
||||
try {
|
||||
HashMap<String, Object> result = new HashMap<>(proj.length);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int pos = 0;
|
||||
for (String p : proj) {
|
||||
switch (types[pos]) {
|
||||
case FIELD_TYPE_NULL:
|
||||
result.put(p, cursor.isNull(pos));
|
||||
break;
|
||||
case FIELD_TYPE_INTEGER:
|
||||
result.put(p, cursor.getLong(pos));
|
||||
break;
|
||||
case FIELD_TYPE_FLOAT:
|
||||
result.put(p, cursor.getFloat(pos));
|
||||
break;
|
||||
case FIELD_TYPE_STRING:
|
||||
result.put(p, cursor.getString(pos));
|
||||
break;
|
||||
case FIELD_TYPE_BLOB:
|
||||
result.put(p, cursor.getBlob(pos));
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
} else {
|
||||
// If no data was found, throw an appropriate exception
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HashMap<String, Object> getUnifiedData(long masterKeyId, String[] proj, int[] types)
|
||||
throws NotFoundException {
|
||||
return getGenericData(KeyRings.buildUnifiedKeyRingUri(masterKeyId), proj, types);
|
||||
}
|
||||
|
||||
public long getMasterKeyId(long subKeyId) throws NotFoundException {
|
||||
return (Long) getGenericData(KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId),
|
||||
KeyRings.MASTER_KEY_ID, FIELD_TYPE_INTEGER);
|
||||
}
|
||||
|
||||
public CachedPublicKeyRing getCachedPublicKeyRing(Uri queryUri) throws PgpKeyNotFoundException {
|
||||
long masterKeyId = new CachedPublicKeyRing(this, queryUri).extractOrGetMasterKeyId();
|
||||
return getCachedPublicKeyRing(masterKeyId);
|
||||
}
|
||||
|
||||
public CachedPublicKeyRing getCachedPublicKeyRing(long id) {
|
||||
return new CachedPublicKeyRing(this, KeyRings.buildUnifiedKeyRingUri(id));
|
||||
}
|
||||
|
||||
public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(long id) throws NotFoundException {
|
||||
return getCanonicalizedPublicKeyRing(KeyRings.buildUnifiedKeyRingUri(id));
|
||||
}
|
||||
|
||||
public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(Uri queryUri) throws NotFoundException {
|
||||
Cursor cursor = contentResolver.query(queryUri,
|
||||
new String[] { KeyRings.MASTER_KEY_ID, KeyRings.VERIFIED }, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
long masterKeyId = cursor.getLong(0);
|
||||
int verified = cursor.getInt(1);
|
||||
|
||||
byte[] publicKeyData = loadPublicKeyRingData(masterKeyId);
|
||||
return new CanonicalizedPublicKeyRing(publicKeyData, verified);
|
||||
} else {
|
||||
throw new NotFoundException("Key not found!");
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(long id) throws NotFoundException {
|
||||
return getCanonicalizedSecretKeyRing(KeyRings.buildUnifiedKeyRingUri(id));
|
||||
}
|
||||
|
||||
public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(Uri queryUri) throws NotFoundException {
|
||||
Cursor cursor = contentResolver.query(queryUri,
|
||||
new String[] { KeyRings.MASTER_KEY_ID, KeyRings.VERIFIED, KeyRings.HAS_ANY_SECRET }, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
long masterKeyId = cursor.getLong(0);
|
||||
int verified = cursor.getInt(1);
|
||||
int hasAnySecret = cursor.getInt(2);
|
||||
if (hasAnySecret == 0) {
|
||||
throw new NotFoundException("No secret key available or unknown public key!");
|
||||
}
|
||||
|
||||
byte[] secretKeyData = loadSecretKeyRingData(masterKeyId);
|
||||
return new CanonicalizedSecretKeyRing(secretKeyData, verified);
|
||||
} else {
|
||||
throw new NotFoundException("Key not found!");
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<String> getConfirmedUserIds(long masterKeyId) throws NotFoundException {
|
||||
Cursor cursor = contentResolver.query(UserPackets.buildUserIdsUri(masterKeyId),
|
||||
new String[]{UserPackets.USER_ID}, UserPackets.VERIFIED + " = " + Certs.VERIFIED_SECRET, null, null
|
||||
);
|
||||
if (cursor == null) {
|
||||
throw new NotFoundException("Key id for requested user ids not found");
|
||||
}
|
||||
|
||||
try {
|
||||
ArrayList<String> userIds = new ArrayList<>(cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
String userId = cursor.getString(0);
|
||||
userIds.add(userId);
|
||||
}
|
||||
|
||||
return userIds;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException, PgpGeneralException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ArmoredOutputStream aos = new ArmoredOutputStream(bos);
|
||||
|
||||
aos.write(data);
|
||||
aos.close();
|
||||
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public String getPublicKeyRingAsArmoredString(long masterKeyId)
|
||||
throws NotFoundException, IOException, PgpGeneralException {
|
||||
byte[] data = loadPublicKeyRingData(masterKeyId);
|
||||
byte[] armoredData = getKeyRingAsArmoredData(data);
|
||||
return new String(armoredData);
|
||||
}
|
||||
|
||||
public byte[] getSecretKeyRingAsArmoredData(long masterKeyId)
|
||||
throws NotFoundException, IOException, PgpGeneralException {
|
||||
byte[] data = loadSecretKeyRingData(masterKeyId);
|
||||
return getKeyRingAsArmoredData(data);
|
||||
}
|
||||
|
||||
public ContentResolver getContentResolver() {
|
||||
return contentResolver;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Long getLastUpdateTime(long masterKeyId) {
|
||||
Cursor lastUpdatedCursor = contentResolver.query(
|
||||
UpdatedKeys.CONTENT_URI,
|
||||
new String[] { UpdatedKeys.LAST_UPDATED },
|
||||
UpdatedKeys.MASTER_KEY_ID + " = ?",
|
||||
new String[] { "" + masterKeyId },
|
||||
null
|
||||
);
|
||||
if (lastUpdatedCursor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long lastUpdateTime;
|
||||
try {
|
||||
if (!lastUpdatedCursor.moveToNext()) {
|
||||
return null;
|
||||
}
|
||||
lastUpdateTime = lastUpdatedCursor.getLong(0);
|
||||
} finally {
|
||||
lastUpdatedCursor.close();
|
||||
}
|
||||
return lastUpdateTime;
|
||||
}
|
||||
|
||||
public final byte[] loadPublicKeyRingData(long masterKeyId) throws NotFoundException {
|
||||
byte[] data = (byte[]) getGenericDataOrNull(KeyRingData.buildPublicKeyRingUri(masterKeyId),
|
||||
KeyRingData.KEY_RING_DATA, FIELD_TYPE_BLOB);
|
||||
|
||||
if (data == null) {
|
||||
try {
|
||||
data = mLocalPublicKeyStorage.readPublicKey(masterKeyId);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Error reading public key from storage!");
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public final byte[] loadSecretKeyRingData(long masterKeyId) throws NotFoundException {
|
||||
byte[] data = (byte[]) getGenericDataOrNull(KeychainContract.KeyRingData.buildSecretKeyRingUri(masterKeyId),
|
||||
KeyRingData.KEY_RING_DATA, FIELD_TYPE_BLOB);
|
||||
|
||||
if (data == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static class NotFoundException extends Exception {
|
||||
public NotFoundException() {
|
||||
}
|
||||
|
||||
public NotFoundException(String name) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,12 @@ import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
public class KeychainContract {
|
||||
|
||||
interface KeyRingsColumns {
|
||||
public interface KeyRingsColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // not a database id
|
||||
String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob
|
||||
}
|
||||
|
||||
interface KeysColumns {
|
||||
public interface KeysColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // not a database id
|
||||
String RANK = "rank";
|
||||
|
||||
@@ -51,18 +51,12 @@ public class KeychainContract {
|
||||
String EXPIRY = "expiry";
|
||||
}
|
||||
|
||||
interface UpdatedKeysColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // not a database id
|
||||
String LAST_UPDATED = "last_updated"; // time since epoch in seconds
|
||||
String SEEN_ON_KEYSERVERS = "seen_on_keyservers";
|
||||
}
|
||||
|
||||
interface KeySignaturesColumns {
|
||||
public interface KeySignaturesColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // not a database id
|
||||
String SIGNER_KEY_ID = "signer_key_id";
|
||||
}
|
||||
|
||||
interface UserPacketsColumns {
|
||||
public interface UserPacketsColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID
|
||||
String TYPE = "type"; // not a database id
|
||||
String USER_ID = "user_id"; // not a database id
|
||||
@@ -75,7 +69,7 @@ public class KeychainContract {
|
||||
String IS_REVOKED = "is_revoked";
|
||||
}
|
||||
|
||||
interface CertsColumns {
|
||||
public interface CertsColumns {
|
||||
String MASTER_KEY_ID = "master_key_id";
|
||||
String RANK = "rank";
|
||||
String KEY_ID_CERTIFIER = "key_id_certifier";
|
||||
@@ -85,34 +79,15 @@ public class KeychainContract {
|
||||
String DATA = "data";
|
||||
}
|
||||
|
||||
interface ApiAppsColumns {
|
||||
String PACKAGE_NAME = "package_name";
|
||||
String PACKAGE_CERTIFICATE = "package_signature";
|
||||
}
|
||||
|
||||
interface ApiAppsAllowedKeysColumns {
|
||||
public interface ApiAppsAllowedKeysColumns {
|
||||
String KEY_ID = "key_id"; // not a database id
|
||||
String PACKAGE_NAME = "package_name"; // foreign key to api_apps.package_name
|
||||
}
|
||||
|
||||
interface OverriddenWarnings {
|
||||
public interface OverriddenWarnings {
|
||||
String IDENTIFIER = "identifier";
|
||||
}
|
||||
|
||||
interface ApiAutocryptPeerColumns {
|
||||
String PACKAGE_NAME = "package_name";
|
||||
String IDENTIFIER = "identifier";
|
||||
String LAST_SEEN = "last_seen";
|
||||
|
||||
String MASTER_KEY_ID = "master_key_id";
|
||||
String LAST_SEEN_KEY = "last_seen_key";
|
||||
String IS_MUTUAL = "is_mutual";
|
||||
|
||||
String GOSSIP_MASTER_KEY_ID = "gossip_master_key_id";
|
||||
String GOSSIP_LAST_SEEN_KEY = "gossip_last_seen_key";
|
||||
String GOSSIP_ORIGIN = "gossip_origin";
|
||||
}
|
||||
|
||||
public static final String CONTENT_AUTHORITY = Constants.PROVIDER_AUTHORITY;
|
||||
|
||||
private static final Uri BASE_CONTENT_URI_INTERNAL = Uri
|
||||
@@ -120,181 +95,40 @@ public class KeychainContract {
|
||||
|
||||
public static final String BASE_KEY_RINGS = "key_rings";
|
||||
|
||||
public static final String BASE_UPDATED_KEYS = "updated_keys";
|
||||
|
||||
public static final String BASE_KEY_SIGNATURES = "key_signatures";
|
||||
|
||||
public static final String PATH_UNIFIED = "unified";
|
||||
|
||||
public static final String PATH_FIND = "find";
|
||||
public static final String PATH_BY_EMAIL = "email";
|
||||
public static final String PATH_BY_SUBKEY = "subkey";
|
||||
public static final String PATH_BY_USER_ID = "user_id";
|
||||
|
||||
public static final String PATH_FILTER = "filter";
|
||||
public static final String PATH_BY_SIGNER = "signer";
|
||||
|
||||
public static final String PATH_PUBLIC = "public";
|
||||
public static final String PATH_SECRET = "secret";
|
||||
public static final String PATH_USER_IDS = "user_ids";
|
||||
public static final String PATH_LINKED_IDS = "linked_ids";
|
||||
public static final String PATH_KEYS = "keys";
|
||||
public static final String PATH_CERTS = "certs";
|
||||
|
||||
public static final String BASE_API_APPS = "api_apps";
|
||||
public static final String PATH_ALLOWED_KEYS = "allowed_keys";
|
||||
|
||||
public static final String PATH_BY_PACKAGE_NAME = "by_package_name";
|
||||
public static final String PATH_BY_KEY_ID = "by_key_id";
|
||||
|
||||
public static final String BASE_AUTOCRYPT_PEERS = "autocrypt_peers";
|
||||
|
||||
public static class KeyRings implements BaseColumns, KeysColumns, UserPacketsColumns {
|
||||
public static final String MASTER_KEY_ID = KeysColumns.MASTER_KEY_ID;
|
||||
public static final String IS_REVOKED = KeysColumns.IS_REVOKED;
|
||||
public static final String IS_SECURE = KeysColumns.IS_SECURE;
|
||||
public static final String VERIFIED = CertsColumns.VERIFIED;
|
||||
public static final String IS_EXPIRED = "is_expired";
|
||||
public static final String HAS_ANY_SECRET = "has_any_secret";
|
||||
public static final String HAS_ENCRYPT = "has_encrypt";
|
||||
public static final String HAS_SIGN_SECRET = "has_sign_secret";
|
||||
public static final String HAS_CERTIFY_SECRET = "has_certify_secret";
|
||||
public static final String HAS_AUTHENTICATE = "has_authenticate";
|
||||
public static final String HAS_AUTHENTICATE_SECRET = "has_authenticate_secret";
|
||||
public static final String HAS_DUPLICATE_USER_ID = "has_duplicate_user_id";
|
||||
public static final String API_KNOWN_TO_PACKAGE_NAMES = "known_to_apps";
|
||||
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
public static final String CONTENT_TYPE
|
||||
= "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.key_rings";
|
||||
public static final String CONTENT_ITEM_TYPE
|
||||
= "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.key_rings";
|
||||
|
||||
public static Uri buildUnifiedKeyRingsUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_UNIFIED).build();
|
||||
}
|
||||
|
||||
public static Uri buildGenericKeyRingUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).build();
|
||||
}
|
||||
|
||||
public static Uri buildGenericKeyRingUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).build();
|
||||
}
|
||||
|
||||
public static Uri buildGenericKeyRingUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).build();
|
||||
}
|
||||
|
||||
public static Uri buildUnifiedKeyRingUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId))
|
||||
.appendPath(PATH_UNIFIED).build();
|
||||
}
|
||||
|
||||
public static Uri buildUnifiedKeyRingUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
|
||||
.appendPath(PATH_UNIFIED).build();
|
||||
}
|
||||
|
||||
public static Uri buildUnifiedKeyRingsFindByEmailUri(String email) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_FIND)
|
||||
.appendPath(PATH_BY_EMAIL).appendPath(email).build();
|
||||
}
|
||||
|
||||
public static Uri buildUnifiedKeyRingsFindByUserIdUri(String query) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_FIND)
|
||||
.appendPath(PATH_BY_USER_ID).appendPath(query).build();
|
||||
}
|
||||
|
||||
public static Uri buildUnifiedKeyRingsFindBySubkeyUri(long subkey) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_FIND)
|
||||
.appendPath(PATH_BY_SUBKEY).appendPath(Long.toString(subkey)).build();
|
||||
}
|
||||
|
||||
public static Uri buildUnifiedKeyRingsFilterBySigner() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_FILTER).appendPath(PATH_BY_SIGNER).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class KeyRingData implements KeyRingsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
public static final String CONTENT_TYPE
|
||||
= "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.key_ring_data";
|
||||
public static final String CONTENT_ITEM_TYPE
|
||||
= "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.key_ring_data";
|
||||
|
||||
public static Uri buildPublicKeyRingUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeyRingUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_PUBLIC).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeyRingUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_PUBLIC).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_SECRET).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_SECRET).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.org.sufficientlysecure.keychain.provider.keychain.keys";
|
||||
|
||||
/**
|
||||
* Use if a single item is returned
|
||||
*/
|
||||
public static final String CONTENT_ITEM_TYPE
|
||||
= "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.keychain.keys";
|
||||
|
||||
public static Uri buildKeysUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_KEYS).build();
|
||||
}
|
||||
|
||||
public static Uri buildKeysUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_KEYS).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class UpdatedKeys implements UpdatedKeysColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_UPDATED_KEYS).build();
|
||||
|
||||
public static final String CONTENT_TYPE
|
||||
= "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.updated_keys";
|
||||
public static final String CONTENT_ITEM_TYPE
|
||||
= "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.updated_keys";
|
||||
}
|
||||
|
||||
public static class KeySignatures implements KeySignaturesColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_SIGNATURES).build();
|
||||
|
||||
public static final String CONTENT_TYPE
|
||||
= "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.key_signatures";
|
||||
}
|
||||
|
||||
public static class UserPackets implements UserPacketsColumns, BaseColumns {
|
||||
@@ -302,116 +136,12 @@ public class KeychainContract {
|
||||
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.org.sufficientlysecure.keychain.provider.user_ids";
|
||||
|
||||
/**
|
||||
* Use if a single item is returned
|
||||
*/
|
||||
public static final String CONTENT_ITEM_TYPE
|
||||
= "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.user_ids";
|
||||
|
||||
public static Uri buildUserIdsUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
|
||||
public static Uri buildUserIdsUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
|
||||
public static Uri buildUserIdsUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
|
||||
public static Uri buildLinkedIdsUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_LINKED_IDS).build();
|
||||
}
|
||||
|
||||
public static Uri buildLinkedIdsUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_LINKED_IDS).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.org.sufficientlysecure.keychain.provider.api_apps";
|
||||
|
||||
/**
|
||||
* Use if a single item is returned
|
||||
*/
|
||||
public static final String CONTENT_ITEM_TYPE
|
||||
= "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.api_apps";
|
||||
|
||||
public static Uri buildByPackageNameUri(String packageName) {
|
||||
return CONTENT_URI.buildUpon().appendEncodedPath(packageName).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiAllowedKeys implements ApiAppsAllowedKeysColumns, 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.org.sufficientlysecure.keychain.provider.api_apps.allowed_keys";
|
||||
|
||||
public static Uri buildBaseUri(String packageName) {
|
||||
return CONTENT_URI.buildUpon().appendEncodedPath(packageName).appendPath(PATH_ALLOWED_KEYS)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiAutocryptPeer implements ApiAutocryptPeerColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_AUTOCRYPT_PEERS).build();
|
||||
public static final String KEY_IS_REVOKED = "key_is_revoked";
|
||||
public static final String KEY_IS_EXPIRED = "key_is_expired";
|
||||
public static final String KEY_IS_VERIFIED = "key_is_verified";
|
||||
public static final String GOSSIP_KEY_IS_REVOKED = "gossip_key_is_revoked";
|
||||
public static final String GOSSIP_KEY_IS_EXPIRED = "gossip_key_is_expired";
|
||||
public static final String GOSSIP_KEY_IS_VERIFIED = "gossip_key_is_verified";
|
||||
|
||||
public static final int GOSSIP_ORIGIN_AUTOCRYPT = 0;
|
||||
public static final int GOSSIP_ORIGIN_SIGNATURE = 10;
|
||||
public static final int GOSSIP_ORIGIN_DEDUP = 20;
|
||||
|
||||
public static Uri buildByKeyUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(uri.getPathSegments().get(1)).build();
|
||||
}
|
||||
|
||||
public static Uri buildByPackageName(String packageName) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName).build();
|
||||
}
|
||||
|
||||
public static Uri buildByPackageNameAndAutocryptId(String packageName, String autocryptPeer) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName).appendPath(autocryptPeer).build();
|
||||
}
|
||||
|
||||
public static Uri buildByMasterKeyId(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(Long.toString(masterKeyId)).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Certs implements CertsColumns, BaseColumns {
|
||||
public static final String USER_ID = UserPacketsColumns.USER_ID;
|
||||
public static final String NAME = UserPacketsColumns.NAME;
|
||||
public static final String EMAIL = UserPacketsColumns.EMAIL;
|
||||
public static final String COMMENT = UserPacketsColumns.COMMENT;
|
||||
public static final String SIGNER_UID = "signer_user_id";
|
||||
|
||||
public static final int UNVERIFIED = 0;
|
||||
public static final int VERIFIED_SECRET = 1;
|
||||
public static final int VERIFIED_SELF = 2;
|
||||
|
||||
@@ -421,24 +151,6 @@ public class KeychainContract {
|
||||
public static Uri buildCertsUri(long masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_CERTS).build();
|
||||
}
|
||||
|
||||
public static Uri buildCertsSpecificUri(long masterKeyId, long rank, long certifier) {
|
||||
return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId))
|
||||
.appendPath(PATH_CERTS).appendPath(Long.toString(rank))
|
||||
.appendPath(Long.toString(certifier)).build();
|
||||
}
|
||||
|
||||
public static Uri buildCertsUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
|
||||
.appendPath(PATH_CERTS).build();
|
||||
}
|
||||
|
||||
public static Uri buildLinkedIdCertsUri(Uri uri, int rank) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
|
||||
.appendPath(PATH_LINKED_IDS).appendPath(Integer.toString(rank))
|
||||
.appendPath(PATH_CERTS).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private KeychainContract() {
|
||||
|
||||
@@ -22,7 +22,6 @@ import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer;
|
||||
|
||||
|
||||
public class KeychainExternalContract {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
|
||||
|
||||
|
||||
public class LastUpdateInteractor {
|
||||
private final ContentResolver contentResolver;
|
||||
private final DatabaseNotifyManager databaseNotifyManager;
|
||||
|
||||
|
||||
public static LastUpdateInteractor create(Context context) {
|
||||
return new LastUpdateInteractor(context.getContentResolver(), DatabaseNotifyManager.create(context));
|
||||
}
|
||||
|
||||
private LastUpdateInteractor(ContentResolver contentResolver, DatabaseNotifyManager databaseNotifyManager) {
|
||||
this.contentResolver = contentResolver;
|
||||
this.databaseNotifyManager = databaseNotifyManager;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getSeenOnKeyservers(long masterKeyId) {
|
||||
Cursor cursor = contentResolver.query(
|
||||
UpdatedKeys.CONTENT_URI,
|
||||
new String[] { UpdatedKeys.SEEN_ON_KEYSERVERS },
|
||||
UpdatedKeys.MASTER_KEY_ID + " = ?",
|
||||
new String[] { "" + masterKeyId },
|
||||
null
|
||||
);
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Boolean seenOnKeyservers;
|
||||
try {
|
||||
if (!cursor.moveToNext()) {
|
||||
return null;
|
||||
}
|
||||
seenOnKeyservers = cursor.isNull(0) ? null : cursor.getInt(0) != 0;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
return seenOnKeyservers;
|
||||
}
|
||||
|
||||
public void resetAllLastUpdatedTimes() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull(UpdatedKeys.LAST_UPDATED);
|
||||
values.putNull(UpdatedKeys.SEEN_ON_KEYSERVERS);
|
||||
contentResolver.update(UpdatedKeys.CONTENT_URI, values, null, null);
|
||||
}
|
||||
|
||||
public Uri renewKeyLastUpdatedTime(long masterKeyId, boolean seenOnKeyservers) {
|
||||
boolean isFirstKeyserverStatusCheck = getSeenOnKeyservers(masterKeyId) == null;
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(UpdatedKeys.LAST_UPDATED, GregorianCalendar.getInstance().getTimeInMillis() / 1000);
|
||||
if (seenOnKeyservers || isFirstKeyserverStatusCheck) {
|
||||
values.put(UpdatedKeys.SEEN_ON_KEYSERVERS, seenOnKeyservers);
|
||||
}
|
||||
|
||||
// this will actually update/replace, doing the right thing™ for seenOnKeyservers value
|
||||
// see `KeychainProvider.insert()`
|
||||
Uri insert = contentResolver.insert(UpdatedKeys.CONTENT_URI, values);
|
||||
databaseNotifyManager.notifyKeyserverStatusChange(masterKeyId);
|
||||
return insert;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.OverriddenWarnings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
|
||||
|
||||
public class OverriddenWarningsRepository {
|
||||
private final Context context;
|
||||
private KeychainDatabase keychainDatabase;
|
||||
|
||||
public static OverriddenWarningsRepository createOverriddenWarningsRepository(Context context) {
|
||||
return new OverriddenWarningsRepository(context);
|
||||
}
|
||||
|
||||
private OverriddenWarningsRepository(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private KeychainDatabase getDb() {
|
||||
if (keychainDatabase == null) {
|
||||
keychainDatabase = new KeychainDatabase(context);
|
||||
}
|
||||
return keychainDatabase;
|
||||
}
|
||||
|
||||
public boolean isWarningOverridden(String identifier) {
|
||||
SQLiteDatabase db = getDb().getReadableDatabase();
|
||||
Cursor cursor = db.query(
|
||||
Tables.OVERRIDDEN_WARNINGS,
|
||||
new String[] { "COUNT(*)" },
|
||||
OverriddenWarnings.IDENTIFIER + " = ?",
|
||||
new String[] { identifier },
|
||||
null, null, null);
|
||||
|
||||
try {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getInt(0) > 0;
|
||||
} finally {
|
||||
cursor.close();
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void putOverride(String identifier) {
|
||||
SQLiteDatabase db = getDb().getWritableDatabase();
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(OverriddenWarnings.IDENTIFIER, identifier);
|
||||
db.replace(Tables.OVERRIDDEN_WARNINGS, null, cv);
|
||||
db.close();
|
||||
}
|
||||
|
||||
public void deleteOverride(String identifier) {
|
||||
SQLiteDatabase db = getDb().getWritableDatabase();
|
||||
db.delete(Tables.OVERRIDDEN_WARNINGS, OverriddenWarnings.IDENTIFIER + " = ?", new String[] { identifier });
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
/** This interface contains the principal methods for database access
|
||||
* from {#android.content.ContentResolver}. It is used to allow substitution
|
||||
* of a ContentResolver in DAOs.
|
||||
*
|
||||
* @see ApiDataAccessObject
|
||||
*/
|
||||
public interface SimpleContentResolverInterface {
|
||||
Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, String sortOrder);
|
||||
|
||||
Uri insert(Uri contentUri, ContentValues values);
|
||||
|
||||
int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs);
|
||||
|
||||
int delete(Uri contentUri, String where, String[] selectionArgs);
|
||||
}
|
||||
@@ -18,8 +18,17 @@
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -29,18 +38,15 @@ import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.Worker;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.DatabaseUtil;
|
||||
import timber.log.Timber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* TemporaryStorageProvider stores decrypted files inside the app's cache directory previously to
|
||||
* sharing them with other applications.
|
||||
@@ -81,16 +87,20 @@ public class TemporaryFileProvider extends ContentProvider {
|
||||
private static Pattern UUID_PATTERN = Pattern.compile("[a-fA-F0-9-]+");
|
||||
|
||||
public static Uri createFile(Context context, String targetName, String mimeType) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName);
|
||||
contentValues.put(TemporaryFileColumns.COLUMN_TYPE, mimeType);
|
||||
return context.getContentResolver().insert(CONTENT_URI, contentValues);
|
||||
contentValues.put(TemporaryFileColumns.COLUMN_TIME, System.currentTimeMillis());
|
||||
Uri resultUri = contentResolver.insert(CONTENT_URI, contentValues);
|
||||
|
||||
scheduleCleanupAfterTtl();
|
||||
return resultUri;
|
||||
}
|
||||
|
||||
public static Uri createFile(Context context, String targetName) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName);
|
||||
return context.getContentResolver().insert(CONTENT_URI, contentValues);
|
||||
return createFile(context, targetName, null);
|
||||
}
|
||||
|
||||
public static Uri createFile(Context context) {
|
||||
@@ -110,15 +120,6 @@ public class TemporaryFileProvider extends ContentProvider {
|
||||
return context.getContentResolver().update(uri, values, null, null);
|
||||
}
|
||||
|
||||
public static int cleanUp(Context context) {
|
||||
Timber.d("Cleaning up temporary files…");
|
||||
return context.getContentResolver().delete(
|
||||
CONTENT_URI,
|
||||
TemporaryFileColumns.COLUMN_TIME + "< ?",
|
||||
new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}
|
||||
);
|
||||
}
|
||||
|
||||
private class TemporaryStorageDatabase extends SQLiteOpenHelper {
|
||||
|
||||
public TemporaryStorageDatabase(Context context) {
|
||||
@@ -247,9 +248,6 @@ public class TemporaryFileProvider extends ContentProvider {
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
if (!values.containsKey(TemporaryFileColumns.COLUMN_TIME)) {
|
||||
values.put(TemporaryFileColumns.COLUMN_TIME, System.currentTimeMillis());
|
||||
}
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
values.put(TemporaryFileColumns.COLUMN_UUID, uuid);
|
||||
int insert = (int) db.getWritableDatabase().insert(TABLE_FILES, null, values);
|
||||
@@ -310,4 +308,34 @@ public class TemporaryFileProvider extends ContentProvider {
|
||||
return openFileHelper(uri, mode);
|
||||
}
|
||||
|
||||
public static void scheduleCleanupAfterTtl() {
|
||||
OneTimeWorkRequest cleanupWork = new OneTimeWorkRequest.Builder(CleanupWorker.class)
|
||||
.setInitialDelay(Constants.TEMPFILE_TTL, TimeUnit.MILLISECONDS).build();
|
||||
WorkManager.getInstance().enqueue(cleanupWork);
|
||||
}
|
||||
|
||||
public static void scheduleCleanupImmediately() {
|
||||
OneTimeWorkRequest cleanupWork = new OneTimeWorkRequest.Builder(CleanupWorker.class).build();
|
||||
WorkManager workManager = WorkManager.getInstance();
|
||||
if (workManager != null) { // it's possible this is null, if this is called in onCreate of secondary processes
|
||||
workManager.enqueue(cleanupWork);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CleanupWorker extends Worker {
|
||||
@NonNull
|
||||
@Override
|
||||
public WorkerResult doWork() {
|
||||
Timber.d("Cleaning up temporary files…");
|
||||
|
||||
ContentResolver contentResolver = getApplicationContext().getContentResolver();
|
||||
contentResolver.delete(
|
||||
CONTENT_URI,
|
||||
TemporaryFileColumns.COLUMN_TIME + " <= ?",
|
||||
new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}
|
||||
);
|
||||
|
||||
return WorkerResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
@@ -25,7 +26,6 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import org.sufficientlysecure.keychain.pgp.DecryptVerifySecurityProblem;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.remote.ui.RemoteBackupActivity;
|
||||
import org.sufficientlysecure.keychain.remote.ui.RemoteDisplayTransferCodeActivity;
|
||||
import org.sufficientlysecure.keychain.remote.ui.RemoteErrorActivity;
|
||||
@@ -92,8 +92,9 @@ public class ApiPendingIntentFactory {
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
PendingIntent createSelectPublicKeyPendingIntent(Intent data, long[] keyIdsArray, ArrayList<String> missingEmails,
|
||||
ArrayList<String> duplicateEmails, boolean noUserIdsCheck) {
|
||||
public PendingIntent createSelectPublicKeyPendingIntent(Intent data, long[] keyIdsArray,
|
||||
ArrayList<String> missingEmails,
|
||||
ArrayList<String> duplicateEmails, boolean noUserIdsCheck) {
|
||||
Intent intent = new Intent(mContext, RemoteSelectPubKeyActivity.class);
|
||||
intent.putExtra(RemoteSelectPubKeyActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray);
|
||||
intent.putExtra(RemoteSelectPubKeyActivity.EXTRA_NO_USER_IDS_CHECK, noUserIdsCheck);
|
||||
@@ -103,7 +104,7 @@ public class ApiPendingIntentFactory {
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
PendingIntent createDeduplicatePendingIntent(String packageName, Intent data, ArrayList<String> duplicateEmails) {
|
||||
public PendingIntent createDeduplicatePendingIntent(String packageName, Intent data, ArrayList<String> duplicateEmails) {
|
||||
Intent intent = new Intent(mContext, RemoteDeduplicateActivity.class);
|
||||
|
||||
intent.putExtra(RemoteDeduplicateActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
@@ -121,7 +122,7 @@ public class ApiPendingIntentFactory {
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
PendingIntent createRequestKeyPermissionPendingIntent(Intent data, String packageName, long... masterKeyIds) {
|
||||
public PendingIntent createRequestKeyPermissionPendingIntent(Intent data, String packageName, long... masterKeyIds) {
|
||||
Intent intent = new Intent(mContext, RequestKeyPermissionActivity.class);
|
||||
intent.putExtra(RequestKeyPermissionActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
intent.putExtra(RequestKeyPermissionActivity.EXTRA_REQUESTED_KEY_IDS, masterKeyIds);
|
||||
@@ -130,24 +131,23 @@ public class ApiPendingIntentFactory {
|
||||
}
|
||||
|
||||
PendingIntent createShowKeyPendingIntent(Intent data, long masterKeyId) {
|
||||
Intent intent = new Intent(mContext, ViewKeyActivity.class);
|
||||
intent.setData(KeychainContract.KeyRings.buildGenericKeyRingUri(masterKeyId));
|
||||
Intent intent = ViewKeyActivity.getViewKeyActivityIntent(mContext, masterKeyId);
|
||||
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
PendingIntent createSelectSignKeyIdLegacyPendingIntent(Intent data, String packageName, String preferredUserId) {
|
||||
public PendingIntent createSelectSignKeyIdLegacyPendingIntent(Intent data, String packageName,
|
||||
String preferredUserId) {
|
||||
Intent intent = new Intent(mContext, SelectSignKeyIdActivity.class);
|
||||
intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName));
|
||||
intent.putExtra(SelectSignKeyIdActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId);
|
||||
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName,
|
||||
public PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName,
|
||||
byte[] packageSignature, String preferredUserId, boolean showAutocryptHint) {
|
||||
Intent intent = new Intent(mContext, RemoteSelectIdKeyActivity.class);
|
||||
intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName));
|
||||
intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature);
|
||||
intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_USER_ID, preferredUserId);
|
||||
@@ -156,9 +156,8 @@ public class ApiPendingIntentFactory {
|
||||
return createInternal(data, intent);
|
||||
}
|
||||
|
||||
PendingIntent createSelectAuthenticationKeyIdPendingIntent(Intent data, String packageName) {
|
||||
public PendingIntent createSelectAuthenticationKeyIdPendingIntent(Intent data, String packageName) {
|
||||
Intent intent = new Intent(mContext, RemoteSelectAuthenticationKeyActivity.class);
|
||||
intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName));
|
||||
intent.putExtra(RemoteSelectAuthenticationKeyActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
|
||||
return createInternal(data, intent);
|
||||
@@ -220,7 +219,7 @@ public class ApiPendingIntentFactory {
|
||||
}
|
||||
}
|
||||
|
||||
PendingIntent createRegisterPendingIntent(Intent data, String packageName, byte[] packageCertificate) {
|
||||
public PendingIntent createRegisterPendingIntent(Intent data, String packageName, byte[] packageCertificate) {
|
||||
Intent intent = new Intent(mContext, RemoteRegisterActivity.class);
|
||||
intent.putExtra(RemoteRegisterActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
intent.putExtra(RemoteRegisterActivity.EXTRA_PACKAGE_SIGNATURE, packageCertificate);
|
||||
|
||||
@@ -35,7 +35,7 @@ import android.os.Binder;
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
@@ -45,13 +45,13 @@ import timber.log.Timber;
|
||||
public class ApiPermissionHelper {
|
||||
|
||||
private final Context mContext;
|
||||
private final ApiDataAccessObject mApiDao;
|
||||
private final ApiAppDao mApiAppDao;
|
||||
private PackageManager mPackageManager;
|
||||
|
||||
public ApiPermissionHelper(Context context, ApiDataAccessObject apiDao) {
|
||||
public ApiPermissionHelper(Context context, ApiAppDao apiAppDao) {
|
||||
mContext = context;
|
||||
mPackageManager = context.getPackageManager();
|
||||
mApiDao = apiDao;
|
||||
mApiAppDao = apiAppDao;
|
||||
}
|
||||
|
||||
public static class WrongPackageCertificateException extends Exception {
|
||||
@@ -206,7 +206,7 @@ public class ApiPermissionHelper {
|
||||
public boolean isPackageAllowed(String packageName) throws WrongPackageCertificateException {
|
||||
Timber.d("isPackageAllowed packageName: " + packageName);
|
||||
|
||||
byte[] storedPackageCert = mApiDao.getApiAppCertificate(packageName);
|
||||
byte[] storedPackageCert = mApiAppDao.getApiAppCertificate(packageName);
|
||||
|
||||
boolean isKnownPackage = storedPackageCert != null;
|
||||
if (!isKnownPackage) {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.remote;
|
||||
|
||||
public class AppSettings {
|
||||
private String mPackageName;
|
||||
private byte[] mPackageCertificate;
|
||||
|
||||
public AppSettings() {
|
||||
|
||||
}
|
||||
|
||||
public AppSettings(String packageName, byte[] packageSignature) {
|
||||
super();
|
||||
this.mPackageName = packageName;
|
||||
this.mPackageCertificate = packageSignature;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return mPackageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.mPackageName = packageName;
|
||||
}
|
||||
|
||||
public byte[] getPackageCertificate() {
|
||||
return mPackageCertificate;
|
||||
}
|
||||
|
||||
public void setPackageCertificate(byte[] packageCertificate) {
|
||||
this.mPackageCertificate = packageCertificate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,52 +2,64 @@ package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import org.openintents.openpgp.AutocryptPeerUpdate;
|
||||
import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer.AutocryptKeyStatus;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import org.sufficientlysecure.keychain.daos.AutocryptPeerDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
class AutocryptInteractor {
|
||||
public class AutocryptInteractor {
|
||||
private static final long AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS = 35 * DateUtils.DAY_IN_MILLIS;
|
||||
|
||||
private AutocryptPeerDataAccessObject autocryptPeerDao;
|
||||
private AutocryptPeerDao autocryptPeerDao;
|
||||
private KeyWritableRepository keyWritableRepository;
|
||||
|
||||
public static AutocryptInteractor getInstance(Context context, AutocryptPeerDataAccessObject autocryptPeerentityDao) {
|
||||
private final String packageName;
|
||||
|
||||
public static AutocryptInteractor getInstance(Context context, String packageName) {
|
||||
AutocryptPeerDao autocryptPeerDao = AutocryptPeerDao.getInstance(context);
|
||||
KeyWritableRepository keyWritableRepository = KeyWritableRepository.create(context);
|
||||
|
||||
return new AutocryptInteractor(autocryptPeerentityDao, keyWritableRepository);
|
||||
return new AutocryptInteractor(autocryptPeerDao, keyWritableRepository, packageName);
|
||||
}
|
||||
|
||||
private AutocryptInteractor(AutocryptPeerDataAccessObject autocryptPeerDao,
|
||||
KeyWritableRepository keyWritableRepository) {
|
||||
private AutocryptInteractor(AutocryptPeerDao autocryptPeerDao,
|
||||
KeyWritableRepository keyWritableRepository, String packageName) {
|
||||
this.autocryptPeerDao = autocryptPeerDao;
|
||||
this.keyWritableRepository = keyWritableRepository;
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
void updateAutocryptPeerState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) {
|
||||
AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId);
|
||||
Date effectiveDate = autocryptPeerUpdate.getEffectiveDate();
|
||||
|
||||
// 1. If the message’s effective date is older than the peers[from-addr].autocrypt_timestamp value, then no changes are required, and the update process terminates.
|
||||
Date lastSeenAutocrypt = autocryptPeerDao.getLastSeenKey(autocryptPeerId);
|
||||
if (lastSeenAutocrypt != null && effectiveDate.compareTo(lastSeenAutocrypt) <= 0) {
|
||||
Date lastSeenKey = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen_key() : null;
|
||||
if (lastSeenKey != null && effectiveDate.compareTo(lastSeenKey) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If the message’s effective date is more recent than peers[from-addr].last_seen then set peers[from-addr].last_seen to the message’s effective date.
|
||||
Date lastSeen = autocryptPeerDao.getLastSeen(autocryptPeerId);
|
||||
Date lastSeen = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen() : null;
|
||||
if (lastSeen == null || effectiveDate.after(lastSeen)) {
|
||||
autocryptPeerDao.updateLastSeen(autocryptPeerId, effectiveDate);
|
||||
autocryptPeerDao.insertOrUpdateLastSeen(packageName, autocryptPeerId, effectiveDate);
|
||||
}
|
||||
|
||||
// 3. If the Autocrypt header is unavailable, no further changes are required and the update process terminates.
|
||||
@@ -66,17 +78,18 @@ class AutocryptInteractor {
|
||||
// 6. Set peers[from-addr].prefer_encrypt to the corresponding prefer-encrypt value of the Autocrypt header.
|
||||
boolean isMutual = autocryptPeerUpdate.getPreferEncrypt() == PreferEncrypt.MUTUAL;
|
||||
|
||||
autocryptPeerDao.updateKey(autocryptPeerId, effectiveDate, newMasterKeyId, isMutual);
|
||||
autocryptPeerDao.updateKey(packageName, autocryptPeerId, effectiveDate, newMasterKeyId, isMutual);
|
||||
}
|
||||
|
||||
void updateAutocryptPeerGossipState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) {
|
||||
AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId);
|
||||
Date effectiveDate = autocryptPeerUpdate.getEffectiveDate();
|
||||
|
||||
// 1. If gossip-addr does not match any recipient in the mail’s To or Cc header, the update process terminates (i.e., header is ignored).
|
||||
// -> This should be taken care of in the mail client that sends us this data!
|
||||
|
||||
// 2. If peers[gossip-addr].gossip_timestamp is more recent than the message’s effective date, then the update process terminates.
|
||||
Date lastSeenGossip = autocryptPeerDao.getLastSeenGossip(autocryptPeerId);
|
||||
Date lastSeenGossip = currentAutocryptPeer != null ? currentAutocryptPeer.gossip_last_seen_key() : null;
|
||||
if (lastSeenGossip != null && lastSeenGossip.after(effectiveDate)) {
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +107,8 @@ class AutocryptInteractor {
|
||||
// 4. Set peers[gossip-addr].gossip_key to the value of the keydata attribute.
|
||||
Long newMasterKeyId = saveKeyringResult.savedMasterKeyId;
|
||||
|
||||
autocryptPeerDao.updateKeyGossipFromAutocrypt(autocryptPeerId, effectiveDate, newMasterKeyId);
|
||||
autocryptPeerDao.updateKeyGossip(packageName, autocryptPeerId, effectiveDate, newMasterKeyId,
|
||||
GossipOrigin.GOSSIP_HEADER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -131,4 +145,100 @@ class AutocryptInteractor {
|
||||
}
|
||||
return uncachedKeyRing;
|
||||
}
|
||||
|
||||
public List<AutocryptRecommendationResult> determineAutocryptRecommendations(String... autocryptIds) {
|
||||
List<AutocryptRecommendationResult> result = new ArrayList<>(autocryptIds.length);
|
||||
|
||||
for (AutocryptKeyStatus autocryptKeyStatus : autocryptPeerDao.getAutocryptKeyStatus(packageName, autocryptIds)) {
|
||||
AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(autocryptKeyStatus);
|
||||
result.add(peerResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Determines Autocrypt "ui-recommendation", according to spec.
|
||||
* See https://autocrypt.org/level1.html#recommendations-for-single-recipient-messages
|
||||
*/
|
||||
private AutocryptRecommendationResult determineAutocryptRecommendation(AutocryptKeyStatus autocryptKeyStatus) {
|
||||
AutocryptRecommendationResult keyRecommendation = determineAutocryptKeyRecommendation(autocryptKeyStatus);
|
||||
if (keyRecommendation != null) return keyRecommendation;
|
||||
|
||||
AutocryptRecommendationResult gossipRecommendation = determineAutocryptGossipRecommendation(autocryptKeyStatus);
|
||||
if (gossipRecommendation != null) return gossipRecommendation;
|
||||
|
||||
return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISABLE, null, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AutocryptRecommendationResult determineAutocryptKeyRecommendation(AutocryptKeyStatus autocryptKeyStatus) {
|
||||
AutocryptPeer autocryptPeer = autocryptKeyStatus.autocryptPeer();
|
||||
|
||||
Long masterKeyId = autocryptPeer.master_key_id();
|
||||
boolean hasKey = masterKeyId != null;
|
||||
boolean isRevoked = autocryptKeyStatus.isKeyRevoked();
|
||||
boolean isExpired = autocryptKeyStatus.isKeyExpired();
|
||||
if (!hasKey || isRevoked || isExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Date lastSeen = autocryptPeer.last_seen();
|
||||
Date lastSeenKey = autocryptPeer.last_seen_key();
|
||||
boolean isVerified = autocryptKeyStatus.isKeyVerified();
|
||||
boolean isLastSeenOlderThanDiscourageTimespan = lastSeen != null && lastSeenKey != null &&
|
||||
lastSeenKey.getTime() < (lastSeen.getTime() - AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS);
|
||||
if (isLastSeenOlderThanDiscourageTimespan) {
|
||||
return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.DISCOURAGED_OLD, masterKeyId, isVerified);
|
||||
}
|
||||
|
||||
boolean isMutual = autocryptPeer.is_mutual();
|
||||
if (isMutual) {
|
||||
return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.MUTUAL, masterKeyId, isVerified);
|
||||
} else {
|
||||
return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.AVAILABLE, masterKeyId, isVerified);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AutocryptRecommendationResult determineAutocryptGossipRecommendation(AutocryptKeyStatus autocryptKeyStatus) {
|
||||
boolean gossipHasKey = autocryptKeyStatus.hasGossipKey();
|
||||
boolean gossipIsRevoked = autocryptKeyStatus.isGossipKeyRevoked();
|
||||
boolean gossipIsExpired = autocryptKeyStatus.isGossipKeyExpired();
|
||||
boolean isVerified = autocryptKeyStatus.isGossipKeyVerified();
|
||||
|
||||
if (!gossipHasKey || gossipIsRevoked || gossipIsExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long masterKeyId = autocryptKeyStatus.autocryptPeer().gossip_master_key_id();
|
||||
return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISCOURAGED_GOSSIP, masterKeyId, isVerified);
|
||||
}
|
||||
|
||||
public void updateKeyGossipFromSignature(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
autocryptPeerDao.updateKeyGossip(packageName, autocryptId, effectiveDate, masterKeyId, GossipOrigin.SIGNATURE);
|
||||
}
|
||||
|
||||
public void updateKeyGossipFromDedup(String autocryptId, long masterKeyId) {
|
||||
autocryptPeerDao.updateKeyGossip(packageName, autocryptId, new Date(), masterKeyId, GossipOrigin.DEDUP);
|
||||
}
|
||||
|
||||
public static class AutocryptRecommendationResult {
|
||||
public final String peerId;
|
||||
public final Long masterKeyId;
|
||||
public final AutocryptState autocryptState;
|
||||
public final boolean isVerified;
|
||||
|
||||
AutocryptRecommendationResult(String peerId, AutocryptState autocryptState, Long masterKeyId,
|
||||
boolean isVerified) {
|
||||
this.peerId = peerId;
|
||||
this.autocryptState = autocryptState;
|
||||
this.masterKeyId = masterKeyId;
|
||||
this.isVerified = isVerified;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum AutocryptState {
|
||||
DISABLE, DISCOURAGED_OLD, DISCOURAGED_GOSSIP, AVAILABLE, MUTUAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import android.arch.persistence.db.SupportSQLiteDatabase;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
@@ -37,44 +38,33 @@ import android.text.TextUtils;
|
||||
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject.AutocryptRecommendationResult;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject.AutocryptState;
|
||||
import org.sufficientlysecure.keychain.provider.DatabaseNotifyManager;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainProvider;
|
||||
import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface;
|
||||
import org.sufficientlysecure.keychain.util.CloseDatabaseCursorFactory;
|
||||
import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptRecommendationResult;
|
||||
import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptState;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class KeychainExternalProvider extends ContentProvider implements SimpleContentResolverInterface {
|
||||
public class KeychainExternalProvider extends ContentProvider {
|
||||
private static final int EMAIL_STATUS = 101;
|
||||
|
||||
private static final int AUTOCRYPT_STATUS = 201;
|
||||
private static final int AUTOCRYPT_STATUS_INTERNAL = 202;
|
||||
|
||||
private static final int API_APPS = 301;
|
||||
private static final int API_APPS_BY_PACKAGE_NAME = 302;
|
||||
|
||||
public static final String TEMP_TABLE_QUERIED_ADDRESSES = "queried_addresses";
|
||||
public static final String TEMP_TABLE_COLUMN_ADDRES = "address";
|
||||
|
||||
|
||||
private UriMatcher uriMatcher;
|
||||
private ApiPermissionHelper apiPermissionHelper;
|
||||
private KeychainProvider internalKeychainProvider;
|
||||
private DatabaseNotifyManager databaseNotifyManager;
|
||||
|
||||
|
||||
/**
|
||||
@@ -111,10 +101,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
throw new NullPointerException("Context can't be null during onCreate!");
|
||||
}
|
||||
|
||||
internalKeychainProvider = new KeychainProvider();
|
||||
internalKeychainProvider.attachInfo(context, null);
|
||||
apiPermissionHelper = new ApiPermissionHelper(context, new ApiDataAccessObject(internalKeychainProvider));
|
||||
databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
apiPermissionHelper = new ApiPermissionHelper(context, ApiAppDao.getInstance(getContext()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -127,13 +114,6 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
switch (match) {
|
||||
case EMAIL_STATUS:
|
||||
return EmailStatus.CONTENT_TYPE;
|
||||
|
||||
case API_APPS:
|
||||
return ApiApps.CONTENT_TYPE;
|
||||
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
return ApiApps.CONTENT_ITEM_TYPE;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
@@ -154,7 +134,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
|
||||
String groupBy = null;
|
||||
|
||||
SQLiteDatabase db = new KeychainDatabase(getContext()).getReadableDatabase();
|
||||
SupportSQLiteDatabase db = KeychainDatabase.getInstance(getContext()).getReadableDatabase();
|
||||
|
||||
String callingPackageName = apiPermissionHelper.getCurrentCallingPackage();
|
||||
|
||||
@@ -169,7 +149,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
ContentValues cv = new ContentValues();
|
||||
for (String address : selectionArgs) {
|
||||
cv.put(TEMP_TABLE_COLUMN_ADDRES, address);
|
||||
db.insert(TEMP_TABLE_QUERIED_ADDRESSES, null, cv);
|
||||
db.insert(TEMP_TABLE_QUERIED_ADDRESSES, SQLiteDatabase.CONFLICT_FAIL, cv);
|
||||
}
|
||||
|
||||
HashMap<String, String> projectionMap = new HashMap<>();
|
||||
@@ -256,7 +236,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
ContentValues cv = new ContentValues();
|
||||
for (String address : selectionArgs) {
|
||||
cv.put(TEMP_TABLE_COLUMN_ADDRES, address);
|
||||
db.insert(TEMP_TABLE_QUERIED_ADDRESSES, null, cv);
|
||||
db.insert(TEMP_TABLE_QUERIED_ADDRESSES, SQLiteDatabase.CONFLICT_FAIL, cv);
|
||||
}
|
||||
|
||||
boolean isWildcardSelector = selectionArgs.length == 1 && selectionArgs[0].contains("%");
|
||||
@@ -278,10 +258,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
throw new UnsupportedOperationException("Cannot wildcard-query autocrypt results!");
|
||||
}
|
||||
if (!isWildcardSelector && queriesAutocryptResult) {
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao =
|
||||
new AutocryptPeerDataAccessObject(internalKeychainProvider, callingPackageName,
|
||||
databaseNotifyManager);
|
||||
fillTempTableWithAutocryptRecommendations(db, autocryptPeerDao, selectionArgs);
|
||||
AutocryptInteractor autocryptInteractor =
|
||||
AutocryptInteractor.getInstance(getContext(), callingPackageName);
|
||||
fillTempTableWithAutocryptRecommendations(db, autocryptInteractor, selectionArgs);
|
||||
}
|
||||
|
||||
HashMap<String, String> projectionMap = new HashMap<>();
|
||||
@@ -321,8 +300,8 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
}
|
||||
|
||||
qb.setStrict(true);
|
||||
qb.setCursorFactory(new CloseDatabaseCursorFactory());
|
||||
Cursor cursor = qb.query(db, projection, null, null, groupBy, null, orderBy);
|
||||
String query = qb.buildQuery(projection, null, groupBy, null, orderBy, null);
|
||||
Cursor cursor = db.query(query);
|
||||
if (cursor != null) {
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
@@ -337,15 +316,15 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private void fillTempTableWithAutocryptRecommendations(SQLiteDatabase db,
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao, String[] peerIds) {
|
||||
private void fillTempTableWithAutocryptRecommendations(SupportSQLiteDatabase db,
|
||||
AutocryptInteractor autocryptInteractor, String[] peerIds) {
|
||||
List<AutocryptRecommendationResult> autocryptStates =
|
||||
autocryptPeerDao.determineAutocryptRecommendations(peerIds);
|
||||
autocryptInteractor.determineAutocryptRecommendations(peerIds);
|
||||
|
||||
fillTempTableWithAutocryptRecommendations(db, autocryptStates);
|
||||
}
|
||||
|
||||
private void fillTempTableWithAutocryptRecommendations(SQLiteDatabase db,
|
||||
private void fillTempTableWithAutocryptRecommendations(SupportSQLiteDatabase db,
|
||||
List<AutocryptRecommendationResult> autocryptRecommendations) {
|
||||
ContentValues cv = new ContentValues();
|
||||
for (AutocryptRecommendationResult peerResult : autocryptRecommendations) {
|
||||
@@ -359,12 +338,12 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
KeychainExternalContract.KEY_STATUS_UNVERIFIED);
|
||||
}
|
||||
|
||||
db.update(TEMP_TABLE_QUERIED_ADDRESSES, cv,TEMP_TABLE_COLUMN_ADDRES + "=?",
|
||||
db.update(TEMP_TABLE_QUERIED_ADDRESSES, SQLiteDatabase.CONFLICT_IGNORE, cv,TEMP_TABLE_COLUMN_ADDRES + "=?",
|
||||
new String[] { peerResult.peerId });
|
||||
}
|
||||
}
|
||||
|
||||
private void fillTempTableWithUidResult(SQLiteDatabase db, boolean isWildcardSelector) {
|
||||
private void fillTempTableWithUidResult(SupportSQLiteDatabase db, boolean isWildcardSelector) {
|
||||
String cmpOperator = isWildcardSelector ? " LIKE " : " = ";
|
||||
long unixSeconds = System.currentTimeMillis() / 1000;
|
||||
db.execSQL("REPLACE INTO " + TEMP_TABLE_QUERIED_ADDRESSES +
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult.AutocryptPeerResult;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
||||
import org.sufficientlysecure.keychain.operations.BackupOperation;
|
||||
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.ExportResult;
|
||||
@@ -66,15 +67,12 @@ import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.SecurityProblem;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
import org.sufficientlysecure.keychain.daos.AutocryptPeerDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
||||
import org.sufficientlysecure.keychain.provider.OverriddenWarningsRepository;
|
||||
import org.sufficientlysecure.keychain.daos.OverriddenWarningsDao;
|
||||
import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResult;
|
||||
import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResultStatus;
|
||||
import org.sufficientlysecure.keychain.service.BackupKeyringParcel;
|
||||
@@ -98,7 +96,7 @@ public class OpenPgpService extends Service {
|
||||
|
||||
private ApiPermissionHelper mApiPermissionHelper;
|
||||
private KeyRepository mKeyRepository;
|
||||
private ApiDataAccessObject mApiDao;
|
||||
private ApiAppDao mApiAppDao;
|
||||
private OpenPgpServiceKeyIdExtractor mKeyIdExtractor;
|
||||
private ApiPendingIntentFactory mApiPendingIntentFactory;
|
||||
|
||||
@@ -106,8 +104,8 @@ public class OpenPgpService extends Service {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mKeyRepository = KeyRepository.create(this);
|
||||
mApiDao = new ApiDataAccessObject(this);
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this, mApiDao);
|
||||
mApiAppDao = ApiAppDao.getInstance(this);
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this, mApiAppDao);
|
||||
mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext());
|
||||
mKeyIdExtractor = OpenPgpServiceKeyIdExtractor.getInstance(getContentResolver(), mApiPendingIntentFactory);
|
||||
}
|
||||
@@ -140,9 +138,9 @@ public class OpenPgpService extends Service {
|
||||
|
||||
// get first usable subkey capable of signing
|
||||
try {
|
||||
long signSubKeyId = mKeyRepository.getCachedPublicKeyRing(signKeyId).getSecretSignId();
|
||||
long signSubKeyId = mKeyRepository.getSecretSignId(signKeyId);
|
||||
pgpData.setSignatureSubKeyId(signSubKeyId);
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
} catch (NotFoundException e) {
|
||||
throw new Exception("signing subkey not found!", e);
|
||||
}
|
||||
}
|
||||
@@ -231,7 +229,7 @@ public class OpenPgpService extends Service {
|
||||
if (signKeyId == Constants.key.none) {
|
||||
throw new Exception("No signing key given");
|
||||
}
|
||||
long signSubKeyId = mKeyRepository.getCachedPublicKeyRing(signKeyId).getSecretSignId();
|
||||
long signSubKeyId = mKeyRepository.getSecretSignId(signKeyId);
|
||||
|
||||
pgpData.setSignatureMasterKeyId(signKeyId)
|
||||
.setSignatureSubKeyId(signSubKeyId)
|
||||
@@ -472,7 +470,7 @@ public class OpenPgpService extends Service {
|
||||
SecurityProblem prioritySecurityProblem = securityProblem.getPrioritySecurityProblem();
|
||||
if (prioritySecurityProblem.isIdentifiable()) {
|
||||
String identifier = prioritySecurityProblem.getIdentifier();
|
||||
boolean isOverridden = OverriddenWarningsRepository.createOverriddenWarningsRepository(this)
|
||||
boolean isOverridden = OverriddenWarningsDao.create(this)
|
||||
.isWarningOverridden(identifier);
|
||||
result.putExtra(OpenPgpApi.RESULT_OVERRIDE_CRYPTO_WARNING, isOverridden);
|
||||
}
|
||||
@@ -585,8 +583,8 @@ public class OpenPgpService extends Service {
|
||||
return signatureResult;
|
||||
}
|
||||
|
||||
AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(),
|
||||
mApiPermissionHelper.getCurrentCallingPackage());
|
||||
AutocryptPeerDao autocryptPeerentityDao =
|
||||
AutocryptPeerDao.getInstance(getBaseContext());
|
||||
Long autocryptPeerMasterKeyId = autocryptPeerentityDao.getMasterKeyIdForAutocryptPeer(autocryptPeerId);
|
||||
|
||||
long masterKeyId = signatureResult.getKeyId();
|
||||
@@ -596,7 +594,9 @@ public class OpenPgpService extends Service {
|
||||
if (effectiveTime.after(now)) {
|
||||
effectiveTime = now;
|
||||
}
|
||||
autocryptPeerentityDao.updateKeyGossipFromSignature(autocryptPeerId, effectiveTime, masterKeyId);
|
||||
AutocryptInteractor autocryptInteractor =
|
||||
AutocryptInteractor.getInstance(this, mApiPermissionHelper.getCurrentCallingPackage());
|
||||
autocryptInteractor.updateKeyGossipFromSignature(autocryptPeerId, effectiveTime, masterKeyId);
|
||||
return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.NEW);
|
||||
} else if (masterKeyId == autocryptPeerMasterKeyId) {
|
||||
return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.OK);
|
||||
@@ -626,10 +626,8 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
|
||||
try {
|
||||
// try to find key, throws NotFoundException if not in db!
|
||||
CanonicalizedPublicKeyRing keyRing =
|
||||
mKeyRepository.getCanonicalizedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(masterKeyId));
|
||||
mKeyRepository.getCanonicalizedPublicKeyRing(masterKeyId);
|
||||
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
@@ -744,17 +742,16 @@ public class OpenPgpService extends Service {
|
||||
result.putExtra(OpenPgpApi.RESULT_SIGN_KEY_ID, signKeyId);
|
||||
|
||||
if (signKeyId != Constants.key.none) {
|
||||
try {
|
||||
CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(signKeyId);
|
||||
String userId = cachedPublicKeyRing.getPrimaryUserId();
|
||||
long creationTime = cachedPublicKeyRing.getCreationTime() * 1000;
|
||||
|
||||
result.putExtra(OpenPgpApi.RESULT_PRIMARY_USER_ID, userId);
|
||||
result.putExtra(OpenPgpApi.RESULT_KEY_CREATION_TIME, creationTime);
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
Timber.e(e, "Error loading key info");
|
||||
return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, e.getMessage());
|
||||
UnifiedKeyInfo unifiedKeyInfo = mKeyRepository.getUnifiedKeyInfo(signKeyId);
|
||||
if (unifiedKeyInfo == null) {
|
||||
Timber.e("Error loading key info");
|
||||
return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, "Signing key not found!");
|
||||
}
|
||||
String userId = unifiedKeyInfo.user_id();
|
||||
long creationTime = unifiedKeyInfo.creation() * 1000;
|
||||
|
||||
result.putExtra(OpenPgpApi.RESULT_PRIMARY_USER_ID, userId);
|
||||
result.putExtra(OpenPgpApi.RESULT_KEY_CREATION_TIME, creationTime);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -860,9 +857,8 @@ public class OpenPgpService extends Service {
|
||||
|
||||
private Intent updateAutocryptPeerImpl(Intent data) {
|
||||
try {
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao = new AutocryptPeerDataAccessObject(getBaseContext(),
|
||||
mApiPermissionHelper.getCurrentCallingPackage());
|
||||
AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance(getBaseContext(), autocryptPeerDao);
|
||||
AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance(
|
||||
getBaseContext(), mApiPermissionHelper.getCurrentCallingPackage());
|
||||
|
||||
if (data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID) &&
|
||||
data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE)) {
|
||||
@@ -915,8 +911,7 @@ public class OpenPgpService extends Service {
|
||||
|
||||
private HashSet<Long> getAllowedKeyIds() {
|
||||
String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
|
||||
return mApiDao.getAllowedKeyIdsForApp(
|
||||
KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg));
|
||||
return mApiAppDao.getAllowedKeyIdsForApp(currentPkg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
|
||||
public class PackageUninstallReceiver extends BroadcastReceiver {
|
||||
|
||||
@@ -34,8 +35,9 @@ public class PackageUninstallReceiver extends BroadcastReceiver {
|
||||
return;
|
||||
}
|
||||
String packageName = uri.getEncodedSchemeSpecificPart();
|
||||
Uri appUri = KeychainContract.ApiApps.buildByPackageNameUri(packageName);
|
||||
context.getContentResolver().delete(appUri, null, null);
|
||||
|
||||
ApiAppDao apiAppDao = ApiAppDao.getInstance(context);
|
||||
apiAppDao.deleteApiApp(packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,19 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.openintents.ssh.authentication.ISshAuthenticationService;
|
||||
@@ -32,15 +40,15 @@ import org.openintents.ssh.authentication.response.PublicKeyResponse;
|
||||
import org.openintents.ssh.authentication.response.SigningResponse;
|
||||
import org.openintents.ssh.authentication.response.SshPublicKeyResponse;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.SshPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ssh.AuthenticationData;
|
||||
@@ -50,20 +58,11 @@ import org.sufficientlysecure.keychain.ssh.AuthenticationResult;
|
||||
import org.sufficientlysecure.keychain.ssh.signature.SshSignatureConverter;
|
||||
import timber.log.Timber;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class SshAuthenticationService extends Service {
|
||||
private static final String TAG = "SshAuthService";
|
||||
|
||||
private ApiPermissionHelper mApiPermissionHelper;
|
||||
private KeyRepository mKeyRepository;
|
||||
private ApiDataAccessObject mApiDao;
|
||||
private ApiAppDao mApiAppDao;
|
||||
private ApiPendingIntentFactory mApiPendingIntentFactory;
|
||||
|
||||
private static final List<Integer> SUPPORTED_VERSIONS = Collections.unmodifiableList(Collections.singletonList(1));
|
||||
@@ -74,9 +73,9 @@ public class SshAuthenticationService extends Service {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this));
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this, ApiAppDao.getInstance(this));
|
||||
mKeyRepository = KeyRepository.create(this);
|
||||
mApiDao = new ApiDataAccessObject(this);
|
||||
mApiAppDao = ApiAppDao.getInstance(this);
|
||||
|
||||
mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext());
|
||||
}
|
||||
@@ -144,23 +143,18 @@ public class SshAuthenticationService extends Service {
|
||||
AuthenticationData.Builder authData = AuthenticationData.builder();
|
||||
authData.setAuthenticationMasterKeyId(masterKeyId);
|
||||
|
||||
CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(masterKeyId);
|
||||
|
||||
long authSubKeyId;
|
||||
int authSubKeyAlgorithm;
|
||||
String authSubKeyCurveOid = null;
|
||||
try {
|
||||
// get first usable subkey capable of authentication
|
||||
authSubKeyId = cachedPublicKeyRing.getSecretAuthenticationId();
|
||||
authSubKeyId = mKeyRepository.getSecretAuthenticationId(masterKeyId);
|
||||
// needed for encoding the resulting signature
|
||||
authSubKeyAlgorithm = getPublicKey(masterKeyId).getAlgorithm();
|
||||
if (authSubKeyAlgorithm == PublicKeyAlgorithmTags.ECDSA) {
|
||||
authSubKeyCurveOid = getPublicKey(masterKeyId).getCurveOid();
|
||||
}
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
return createExceptionErrorResult(SshAuthenticationApiError.NO_AUTH_KEY,
|
||||
"authentication key for master key id not found in keychain", e);
|
||||
} catch (KeyRepository.NotFoundException e) {
|
||||
} catch (NotFoundException e) {
|
||||
return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY,
|
||||
"Key for master key id not found", e);
|
||||
}
|
||||
@@ -272,7 +266,7 @@ public class SshAuthenticationService extends Service {
|
||||
|
||||
try {
|
||||
description = getDescription(masterKeyId);
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
} catch (NotFoundException e) {
|
||||
return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY,
|
||||
"Could not create description", e);
|
||||
}
|
||||
@@ -372,21 +366,21 @@ public class SshAuthenticationService extends Service {
|
||||
return new SshPublicKeyResponse(sshPublicKeyBlob).toIntent();
|
||||
}
|
||||
|
||||
private CanonicalizedPublicKey getPublicKey(long masterKeyId)
|
||||
throws PgpKeyNotFoundException, KeyRepository.NotFoundException {
|
||||
private CanonicalizedPublicKey getPublicKey(long masterKeyId) throws NotFoundException {
|
||||
KeyRepository keyRepository = KeyRepository.create(getApplicationContext());
|
||||
long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId)
|
||||
.getAuthenticationId();
|
||||
return keyRepository.getCanonicalizedPublicKeyRing(masterKeyId)
|
||||
.getPublicKey(authSubKeyId);
|
||||
UnifiedKeyInfo unifiedKeyInfo = keyRepository.getUnifiedKeyInfo(masterKeyId);
|
||||
if (unifiedKeyInfo == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return keyRepository.getCanonicalizedPublicKeyRing(masterKeyId).getPublicKey(unifiedKeyInfo.has_auth_key_int());
|
||||
}
|
||||
|
||||
private String getDescription(long masterKeyId) throws PgpKeyNotFoundException {
|
||||
CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(masterKeyId);
|
||||
private String getDescription(long masterKeyId) throws NotFoundException {
|
||||
UnifiedKeyInfo unifiedKeyInfo = mKeyRepository.getUnifiedKeyInfo(masterKeyId);
|
||||
|
||||
String description = "";
|
||||
long authSubKeyId = cachedPublicKeyRing.getSecretAuthenticationId();
|
||||
description += cachedPublicKeyRing.getPrimaryUserId();
|
||||
long authSubKeyId = mKeyRepository.getSecretAuthenticationId(masterKeyId);
|
||||
description += unifiedKeyInfo.user_id();
|
||||
description += " (" + Long.toHexString(authSubKeyId) + ")";
|
||||
|
||||
return description;
|
||||
@@ -394,7 +388,7 @@ public class SshAuthenticationService extends Service {
|
||||
|
||||
private HashSet<Long> getAllowedKeyIds() {
|
||||
String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
|
||||
return mApiDao.getAllowedKeyIdsForApp(KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg));
|
||||
return mApiAppDao.getAllowedKeyIdsForApp(currentPkg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,7 +26,6 @@ import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.view.Menu;
|
||||
@@ -36,24 +35,26 @@ import android.widget.TextView;
|
||||
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.model.ApiApp;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.AdvancedAppSettingsDialogFragment;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class AppSettingsActivity extends BaseActivity {
|
||||
private Uri mAppUri;
|
||||
public static final String EXTRA_PACKAGE_NAME = "package_name";
|
||||
|
||||
private String packageName;
|
||||
|
||||
private TextView mAppNameView;
|
||||
private ImageView mAppIconView;
|
||||
|
||||
|
||||
// model
|
||||
AppSettings mAppSettings;
|
||||
ApiApp mApiApp;
|
||||
private ApiAppDao apiAppDao;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -68,15 +69,16 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
setTitle(null);
|
||||
|
||||
Intent intent = getIntent();
|
||||
mAppUri = intent.getData();
|
||||
if (mAppUri == null) {
|
||||
Timber.e("Intent data missing. Should be Uri of app!");
|
||||
packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||
if (packageName == null) {
|
||||
Timber.e("Required extra package_name missing!");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Timber.d("uri: %s", mAppUri);
|
||||
loadData(savedInstanceState, mAppUri);
|
||||
apiAppDao = ApiAppDao.getInstance(this);
|
||||
|
||||
loadData(savedInstanceState);
|
||||
}
|
||||
|
||||
private void save() {
|
||||
@@ -138,7 +140,7 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
// advanced info: package certificate SHA-256
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(mAppSettings.getPackageCertificate());
|
||||
md.update(mApiApp.package_signature());
|
||||
byte[] digest = md.digest();
|
||||
certificate = new String(Hex.encode(digest));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
@@ -146,7 +148,7 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
AdvancedAppSettingsDialogFragment dialogFragment =
|
||||
AdvancedAppSettingsDialogFragment.newInstance(mAppSettings.getPackageName(), certificate);
|
||||
AdvancedAppSettingsDialogFragment.newInstance(mApiApp.package_name(), certificate);
|
||||
|
||||
dialogFragment.show(getSupportFragmentManager(), "advancedDialog");
|
||||
}
|
||||
@@ -155,7 +157,7 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
Intent i;
|
||||
PackageManager manager = getPackageManager();
|
||||
try {
|
||||
i = manager.getLaunchIntentForPackage(mAppSettings.getPackageName());
|
||||
i = manager.getLaunchIntentForPackage(mApiApp.package_name());
|
||||
if (i == null)
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
// start like the Android launcher would do
|
||||
@@ -167,31 +169,29 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData(Bundle savedInstanceState, Uri appUri) {
|
||||
mAppSettings = new ApiDataAccessObject(this).getApiAppSettings(appUri);
|
||||
private void loadData(Bundle savedInstanceState) {
|
||||
mApiApp = apiAppDao.getApiApp(packageName);
|
||||
|
||||
// get application name and icon from package manager
|
||||
String appName;
|
||||
Drawable appIcon = null;
|
||||
PackageManager pm = getApplicationContext().getPackageManager();
|
||||
try {
|
||||
ApplicationInfo ai = pm.getApplicationInfo(mAppSettings.getPackageName(), 0);
|
||||
ApplicationInfo ai = pm.getApplicationInfo(mApiApp.package_name(), 0);
|
||||
|
||||
appName = (String) pm.getApplicationLabel(ai);
|
||||
appIcon = pm.getApplicationIcon(ai);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// fallback
|
||||
appName = mAppSettings.getPackageName();
|
||||
appName = mApiApp.package_name();
|
||||
}
|
||||
mAppNameView.setText(appName);
|
||||
mAppIconView.setImageDrawable(appIcon);
|
||||
|
||||
Uri allowedKeysUri = appUri.buildUpon().appendPath(KeychainContract.PATH_ALLOWED_KEYS).build();
|
||||
Timber.d("allowedKeysUri: " + allowedKeysUri);
|
||||
startListFragments(savedInstanceState, allowedKeysUri);
|
||||
startListFragments(savedInstanceState);
|
||||
}
|
||||
|
||||
private void startListFragments(Bundle savedInstanceState, Uri allowedKeysUri) {
|
||||
private void startListFragments(Bundle savedInstanceState) {
|
||||
// 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.
|
||||
@@ -199,7 +199,8 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
AppSettingsAllowedKeysListFragment allowedKeysFragment = AppSettingsAllowedKeysListFragment.newInstance(allowedKeysUri);
|
||||
// Create an instance of the fragments
|
||||
AppSettingsAllowedKeysListFragment allowedKeysFragment = AppSettingsAllowedKeysListFragment.newInstance(packageName);
|
||||
// Add the fragment to the 'fragment_container' FrameLayout
|
||||
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
@@ -210,9 +211,7 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void revokeAccess() {
|
||||
if (getContentResolver().delete(mAppUri, null, null) <= 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
apiAppDao.deleteApiApp(packageName);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,48 +18,36 @@
|
||||
package org.sufficientlysecure.keychain.remote.ui;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeySelectableAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.widget.FixedListView;
|
||||
import timber.log.Timber;
|
||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
||||
import org.sufficientlysecure.keychain.daos.ApiAppDao;
|
||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyChoiceAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.base.RecyclerFragment;
|
||||
import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel;
|
||||
|
||||
|
||||
public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
private static final String ARG_DATA_URI = "uri";
|
||||
public class AppSettingsAllowedKeysListFragment extends RecyclerFragment<KeyChoiceAdapter> {
|
||||
private static final String ARG_PACKAGE_NAME = "package_name";
|
||||
|
||||
private KeySelectableAdapter mAdapter;
|
||||
private ApiDataAccessObject mApiDao;
|
||||
private KeyChoiceAdapter keyChoiceAdapter;
|
||||
private ApiAppDao apiAppDao;
|
||||
|
||||
private Uri mDataUri;
|
||||
private String packageName;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static AppSettingsAllowedKeysListFragment newInstance(Uri dataUri) {
|
||||
public static AppSettingsAllowedKeysListFragment newInstance(String packageName) {
|
||||
AppSettingsAllowedKeysListFragment frag = new AppSettingsAllowedKeysListFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putParcelable(ARG_DATA_URI, dataUri);
|
||||
|
||||
args.putString(ARG_PACKAGE_NAME, packageName);
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
@@ -69,112 +57,48 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mApiDao = new ApiDataAccessObject(getActivity());
|
||||
apiAppDao = ApiAppDao.getInstance(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View layout = super.onCreateView(inflater, container, savedInstanceState);
|
||||
ListView lv = layout.findViewById(android.R.id.list);
|
||||
ViewGroup parent = (ViewGroup) lv.getParent();
|
||||
|
||||
/*
|
||||
* http://stackoverflow.com/a/15880684
|
||||
* Remove ListView and add FixedListView in its place.
|
||||
* This is done here programatically to be still able to use the progressBar of ListFragment.
|
||||
*
|
||||
* We want FixedListView to be able to put this ListFragment inside a ScrollView
|
||||
*/
|
||||
int lvIndex = parent.indexOfChild(lv);
|
||||
parent.removeViewAt(lvIndex);
|
||||
FixedListView newLv = new FixedListView(getActivity());
|
||||
newLv.setId(android.R.id.list);
|
||||
parent.addView(newLv, lvIndex, lv.getLayoutParams());
|
||||
return layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define Adapter and Loader on create of Activity
|
||||
*/
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mDataUri = getArguments().getParcelable(ARG_DATA_URI);
|
||||
packageName = getArguments().getString(ARG_PACKAGE_NAME);
|
||||
|
||||
// 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));
|
||||
|
||||
Set<Long> checked = mApiDao.getAllowedKeyIdsForApp(mDataUri);
|
||||
mAdapter = new KeySelectableAdapter(getActivity(), null, 0, checked);
|
||||
setListAdapter(mAdapter);
|
||||
getListView().setOnItemClickListener(mAdapter);
|
||||
getRecyclerView().setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
|
||||
// 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);
|
||||
hideList(false);
|
||||
|
||||
KeyRepository keyRepository = KeyRepository.create(requireContext());
|
||||
GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class);
|
||||
LiveData<List<UnifiedKeyInfo>> liveData =
|
||||
viewModel.getGenericLiveData(requireContext(), keyRepository::getAllUnifiedKeyInfoWithSecret);
|
||||
liveData.observe(this, this::onLoadUnifiedKeyData);
|
||||
}
|
||||
/** Returns all selected master key ids. */
|
||||
public Set<Long> getSelectedMasterKeyIds() {
|
||||
return mAdapter.getSelectedMasterKeyIds();
|
||||
}
|
||||
|
||||
/** Returns all selected user ids.
|
||||
public String[] getSelectedUserIds() {
|
||||
Vector<String> userIds = new Vector<>();
|
||||
for (int i = 0; i < getListView().getCount(); ++i) {
|
||||
if (getListView().isItemChecked(i)) {
|
||||
userIds.add(mAdapter.getUserId(i));
|
||||
}
|
||||
}
|
||||
|
||||
// make empty array to not return null
|
||||
String userIdArray[] = new String[0];
|
||||
return userIds.toArray(userIdArray);
|
||||
} */
|
||||
|
||||
public void saveAllowedKeys() {
|
||||
try {
|
||||
mApiDao.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds());
|
||||
} catch (RemoteException | OperationApplicationException e) {
|
||||
Timber.e(e, "Problem saving allowed key ids!");
|
||||
}
|
||||
Set<Long> longs = keyChoiceAdapter.getSelectionIds();
|
||||
apiAppDao.saveAllowedKeyIdsForApp(packageName, longs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int loaderId, Bundle data) {
|
||||
Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri();
|
||||
String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1";
|
||||
|
||||
return new CursorLoader(getActivity(), baseUri, KeyAdapter.PROJECTION, where, null, null);
|
||||
}
|
||||
|
||||
@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);
|
||||
public void onLoadUnifiedKeyData(List<UnifiedKeyInfo> data) {
|
||||
if (keyChoiceAdapter == null) {
|
||||
keyChoiceAdapter = KeyChoiceAdapter.createMultiChoiceAdapter(requireContext(), data, null);
|
||||
setAdapter(keyChoiceAdapter);
|
||||
Set<Long> checkedIds = apiAppDao.getAllowedKeyIdsForApp(packageName);
|
||||
keyChoiceAdapter.setSelectionByIds(checkedIds);
|
||||
} else {
|
||||
setListShownNoAnimation(true);
|
||||
keyChoiceAdapter.setUnifiedKeyInfoItems(data);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
boolean animateShowList = !isResumed();
|
||||
showList(animateShowList);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
||||
*
|
||||
* 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.remote.ui;
|
||||
|
||||
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.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
import timber.log.Timber;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class AppSettingsHeaderFragment extends Fragment {
|
||||
|
||||
// model
|
||||
private AppSettings mAppSettings;
|
||||
|
||||
// view
|
||||
private TextView mAppNameView;
|
||||
private ImageView mAppIconView;
|
||||
private TextView mPackageName;
|
||||
private TextView mPackageCertificate;
|
||||
|
||||
public AppSettings getAppSettings() {
|
||||
return mAppSettings;
|
||||
}
|
||||
|
||||
public void setAppSettings(AppSettings appSettings) {
|
||||
this.mAppSettings = appSettings;
|
||||
updateView(appSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
mAppNameView = view.findViewById(R.id.api_app_settings_app_name);
|
||||
mAppIconView = view.findViewById(R.id.api_app_settings_app_icon);
|
||||
mPackageName = view.findViewById(R.id.api_app_settings_package_name);
|
||||
mPackageCertificate = view.findViewById(R.id.api_app_settings_package_certificate);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void updateView(AppSettings appSettings) {
|
||||
// get application name and icon from package manager
|
||||
String appName;
|
||||
Drawable appIcon = null;
|
||||
PackageManager pm = getActivity().getApplicationContext().getPackageManager();
|
||||
try {
|
||||
ApplicationInfo ai = pm.getApplicationInfo(appSettings.getPackageName(), 0);
|
||||
|
||||
appName = (String) pm.getApplicationLabel(ai);
|
||||
appIcon = pm.getApplicationIcon(ai);
|
||||
} catch (NameNotFoundException e) {
|
||||
// fallback
|
||||
appName = appSettings.getPackageName();
|
||||
}
|
||||
mAppNameView.setText(appName);
|
||||
mAppIconView.setImageDrawable(appIcon);
|
||||
|
||||
// advanced info: package name
|
||||
mPackageName.setText(appSettings.getPackageName());
|
||||
|
||||
// advanced info: package signature SHA-256
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(appSettings.getPackageCertificate());
|
||||
byte[] digest = md.digest();
|
||||
String signature = new String(Hex.encode(digest));
|
||||
|
||||
mPackageCertificate.setText(signature);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Timber.e(e, "Should not happen!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -17,87 +17,77 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote.ui;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorJoiner;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.Adapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.livedata.ApiAppsLiveData;
|
||||
import org.sufficientlysecure.keychain.livedata.ApiAppsLiveData.ListedApp;
|
||||
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment.ApiAppAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.base.RecyclerFragment;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class AppsListFragment extends ListFragment implements
|
||||
LoaderManager.LoaderCallbacks<Cursor>, OnItemClickListener {
|
||||
|
||||
AppsAdapter mAdapter;
|
||||
public class AppsListFragment extends RecyclerFragment<ApiAppAdapter> {
|
||||
private ApiAppAdapter adapter;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
getListView().setOnItemClickListener(this);
|
||||
|
||||
// NOTE: No setEmptyText(), we always have the default entries
|
||||
|
||||
// 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 AppsAdapter(getActivity(), null, 0);
|
||||
setListAdapter(mAdapter);
|
||||
adapter = new ApiAppAdapter(getActivity());
|
||||
setAdapter(adapter);
|
||||
setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
|
||||
|
||||
// NOTE: Loader is started in onResume!
|
||||
ApiAppsViewModel viewModel = ViewModelProviders.of(this).get(ApiAppsViewModel.class);
|
||||
viewModel.getListedAppLiveData(requireContext()).observe(this, this::onLoad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// Start out with a progress indicator.
|
||||
setListShown(false);
|
||||
|
||||
// After coming back from Google Play -> reload
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
private void onLoad(List<ListedApp> apiApps) {
|
||||
if (apiApps == null) {
|
||||
hideList(false);
|
||||
adapter.setData(null);
|
||||
return;
|
||||
}
|
||||
adapter.setData(apiApps);
|
||||
showList(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
String selectedPackageName = mAdapter.getItemPackageName(position);
|
||||
boolean installed = mAdapter.getItemIsInstalled(position);
|
||||
boolean registered = mAdapter.getItemIsRegistered(position);
|
||||
public void onItemClick(int position) {
|
||||
ListedApp listedApp = adapter.data.get(position);
|
||||
|
||||
if (installed) {
|
||||
if (registered) {
|
||||
if (listedApp.isInstalled) {
|
||||
if (listedApp.isRegistered) {
|
||||
// Edit app settings
|
||||
Intent intent = new Intent(getActivity(), AppSettingsActivity.class);
|
||||
intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(selectedPackageName));
|
||||
intent.putExtra(AppSettingsActivity.EXTRA_PACKAGE_NAME, listedApp.packageName);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
Intent i;
|
||||
PackageManager manager = getActivity().getPackageManager();
|
||||
PackageManager manager = requireActivity().getPackageManager();
|
||||
try {
|
||||
i = manager.getLaunchIntentForPackage(selectedPackageName);
|
||||
i = manager.getLaunchIntentForPackage(listedApp.packageName);
|
||||
if (i == null) {
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
}
|
||||
@@ -112,255 +102,81 @@ public class AppsListFragment extends ListFragment implements
|
||||
} else {
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=" + selectedPackageName)));
|
||||
Uri.parse("market://details?id=" + listedApp.packageName)));
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=" + selectedPackageName)));
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=" + listedApp.packageName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TEMP_COLUMN_NAME = "NAME";
|
||||
private static final String TEMP_COLUMN_INSTALLED = "INSTALLED";
|
||||
private static final String TEMP_COLUMN_REGISTERED = "REGISTERED";
|
||||
private static final String TEMP_COLUMN_ICON_RES_ID = "ICON_RES_ID";
|
||||
public class ApiAppAdapter extends Adapter<ApiAppViewHolder> {
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
static final String[] PROJECTION = new String[]{
|
||||
ApiApps._ID, // 0
|
||||
ApiApps.PACKAGE_NAME, // 1
|
||||
"null as " + TEMP_COLUMN_NAME, // installed apps can retrieve app name from Android OS
|
||||
"0 as " + TEMP_COLUMN_INSTALLED, // changed later in cursor joiner
|
||||
"1 as " + TEMP_COLUMN_REGISTERED, // if it is in db it is registered
|
||||
"0 as " + TEMP_COLUMN_ICON_RES_ID // not used
|
||||
};
|
||||
private List<ListedApp> data;
|
||||
|
||||
private static final int INDEX_ID = 0;
|
||||
private static final int INDEX_PACKAGE_NAME = 1;
|
||||
private static final int INDEX_NAME = 2;
|
||||
private static final int INDEX_INSTALLED = 3;
|
||||
private static final int INDEX_REGISTERED = 4;
|
||||
private static final int INDEX_ICON_RES_ID = 5;
|
||||
ApiAppAdapter(Context context) {
|
||||
super();
|
||||
|
||||
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 AppsLoader(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);
|
||||
|
||||
// The list should now be shown.
|
||||
setListShown(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Besides the queried cursor with all registered apps, this loader also returns non-installed
|
||||
* proposed apps using a MatrixCursor.
|
||||
*/
|
||||
private static class AppsLoader extends CursorLoader {
|
||||
|
||||
public AppsLoader(Context context) {
|
||||
super(context);
|
||||
inflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
public AppsLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
super(context, uri, projection, selection, selectionArgs, sortOrder);
|
||||
@NonNull
|
||||
@Override
|
||||
public ApiAppViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ApiAppViewHolder(inflater.inflate(R.layout.api_apps_adapter_list_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadInBackground() {
|
||||
// Load registered apps from content provider
|
||||
Cursor data = super.loadInBackground();
|
||||
|
||||
MatrixCursor availableAppsCursor = new MatrixCursor(new String[]{
|
||||
ApiApps._ID,
|
||||
ApiApps.PACKAGE_NAME,
|
||||
TEMP_COLUMN_NAME,
|
||||
TEMP_COLUMN_INSTALLED,
|
||||
TEMP_COLUMN_REGISTERED,
|
||||
TEMP_COLUMN_ICON_RES_ID
|
||||
});
|
||||
// NOTE: SORT ascending by package name, this is REQUIRED for CursorJoiner!
|
||||
// Drawables taken from projects res/drawables-xxhdpi/ic_launcher.png
|
||||
availableAppsCursor.addRow(new Object[]{1, "com.fsck.k9", "K-9 Mail", 0, 0, R.drawable.apps_k9});
|
||||
availableAppsCursor.addRow(new Object[]{1, "com.zeapo.pwdstore", "Password Store", 0, 0, R.drawable.apps_password_store});
|
||||
availableAppsCursor.addRow(new Object[]{1, "eu.siacs.conversations", "Conversations (Instant Messaging)", 0, 0, R.drawable.apps_conversations});
|
||||
|
||||
MatrixCursor mergedCursor = new MatrixCursor(new String[]{
|
||||
ApiApps._ID,
|
||||
ApiApps.PACKAGE_NAME,
|
||||
TEMP_COLUMN_NAME,
|
||||
TEMP_COLUMN_INSTALLED,
|
||||
TEMP_COLUMN_REGISTERED,
|
||||
TEMP_COLUMN_ICON_RES_ID
|
||||
});
|
||||
|
||||
CursorJoiner joiner = new CursorJoiner(
|
||||
availableAppsCursor,
|
||||
new String[]{ApiApps.PACKAGE_NAME},
|
||||
data,
|
||||
new String[]{ApiApps.PACKAGE_NAME});
|
||||
for (CursorJoiner.Result joinerResult : joiner) {
|
||||
switch (joinerResult) {
|
||||
case LEFT: {
|
||||
// handle case where a row in availableAppsCursor is unique
|
||||
String packageName = availableAppsCursor.getString(INDEX_PACKAGE_NAME);
|
||||
|
||||
mergedCursor.addRow(new Object[]{
|
||||
1, // no need for unique _ID
|
||||
packageName,
|
||||
availableAppsCursor.getString(INDEX_NAME),
|
||||
isInstalled(packageName),
|
||||
0,
|
||||
availableAppsCursor.getInt(INDEX_ICON_RES_ID)
|
||||
});
|
||||
break;
|
||||
}
|
||||
case RIGHT: {
|
||||
// handle case where a row in data is unique
|
||||
String packageName = data.getString(INDEX_PACKAGE_NAME);
|
||||
|
||||
mergedCursor.addRow(new Object[]{
|
||||
1, // no need for unique _ID
|
||||
packageName,
|
||||
null,
|
||||
isInstalled(packageName),
|
||||
1, // registered!
|
||||
R.mipmap.ic_launcher // icon is retrieved later
|
||||
});
|
||||
break;
|
||||
}
|
||||
case BOTH: {
|
||||
// handle case where a row with the same key is in both cursors
|
||||
String packageName = data.getString(INDEX_PACKAGE_NAME);
|
||||
|
||||
String name;
|
||||
if (isInstalled(packageName) == 1) {
|
||||
name = data.getString(INDEX_NAME);
|
||||
} else {
|
||||
// if not installed take name from available apps list
|
||||
name = availableAppsCursor.getString(INDEX_NAME);
|
||||
}
|
||||
|
||||
mergedCursor.addRow(new Object[]{
|
||||
1, // no need for unique _ID
|
||||
packageName,
|
||||
name,
|
||||
isInstalled(packageName),
|
||||
1, // registered!
|
||||
R.mipmap.ic_launcher // icon is retrieved later
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedCursor;
|
||||
public void onBindViewHolder(@NonNull ApiAppViewHolder holder, int position) {
|
||||
ListedApp item = data.get(position);
|
||||
holder.bind(item);
|
||||
}
|
||||
|
||||
private int isInstalled(String packageName) {
|
||||
try {
|
||||
getContext().getPackageManager().getApplicationInfo(packageName, 0);
|
||||
return 1;
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
return 0;
|
||||
}
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data != null ? data.size() : 0;
|
||||
}
|
||||
|
||||
public void setData(List<ListedApp> data) {
|
||||
this.data = data;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private class AppsAdapter extends CursorAdapter {
|
||||
public class ApiAppViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView text;
|
||||
private final ImageView icon;
|
||||
private final ImageView installIcon;
|
||||
|
||||
private LayoutInflater mInflater;
|
||||
private PackageManager mPM;
|
||||
ApiAppViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
public AppsAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
mPM = context.getApplicationContext().getPackageManager();
|
||||
text = itemView.findViewById(R.id.api_apps_adapter_item_name);
|
||||
icon = itemView.findViewById(R.id.api_apps_adapter_item_icon);
|
||||
installIcon = itemView.findViewById(R.id.api_apps_adapter_install_icon);
|
||||
itemView.setOnClickListener((View view) -> onItemClick(getAdapterPosition()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to CursorAdapter.getItemId().
|
||||
* Required to build Uris for api apps, which are not based on row ids
|
||||
*/
|
||||
public String getItemPackageName(int position) {
|
||||
if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
|
||||
return mCursor.getString(INDEX_PACKAGE_NAME);
|
||||
void bind(ListedApp listedApp) {
|
||||
text.setText(listedApp.readableName);
|
||||
if (listedApp.applicationIconRes != null) {
|
||||
icon.setImageResource(listedApp.applicationIconRes);
|
||||
} else {
|
||||
return null;
|
||||
icon.setImageDrawable(listedApp.applicationIcon);
|
||||
}
|
||||
installIcon.setVisibility(listedApp.isInstalled ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getItemIsInstalled(int position) {
|
||||
return mDataValid && mCursor != null
|
||||
&& mCursor.moveToPosition(position) && (mCursor.getInt(INDEX_INSTALLED) == 1);
|
||||
}
|
||||
public static class ApiAppsViewModel extends ViewModel {
|
||||
LiveData<List<ListedApp>> listedAppLiveData;
|
||||
|
||||
public boolean getItemIsRegistered(int position) {
|
||||
return mDataValid && mCursor != null
|
||||
&& mCursor.moveToPosition(position) && (mCursor.getInt(INDEX_REGISTERED) == 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
TextView text = view.findViewById(R.id.api_apps_adapter_item_name);
|
||||
ImageView icon = view.findViewById(R.id.api_apps_adapter_item_icon);
|
||||
ImageView installIcon = view.findViewById(R.id.api_apps_adapter_install_icon);
|
||||
|
||||
String packageName = cursor.getString(INDEX_PACKAGE_NAME);
|
||||
Timber.d("packageName: " + packageName);
|
||||
int installed = cursor.getInt(INDEX_INSTALLED);
|
||||
String name = cursor.getString(INDEX_NAME);
|
||||
int iconResName = cursor.getInt(INDEX_ICON_RES_ID);
|
||||
|
||||
// get application name and icon
|
||||
try {
|
||||
ApplicationInfo ai = mPM.getApplicationInfo(packageName, 0);
|
||||
|
||||
text.setText(mPM.getApplicationLabel(ai));
|
||||
icon.setImageDrawable(mPM.getApplicationIcon(ai));
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
// fallback
|
||||
if (name == null) {
|
||||
text.setText(packageName);
|
||||
} else {
|
||||
text.setText(name);
|
||||
try {
|
||||
icon.setImageDrawable(getResources().getDrawable(iconResName));
|
||||
} catch (Resources.NotFoundException e1) {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
LiveData<List<ListedApp>> getListedAppLiveData(Context context) {
|
||||
if (listedAppLiveData == null) {
|
||||
listedAppLiveData = new ApiAppsLiveData(context);
|
||||
}
|
||||
|
||||
if (installed == 1) {
|
||||
installIcon.setVisibility(View.GONE);
|
||||
} else {
|
||||
installIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.api_apps_adapter_list_item, null);
|
||||
return listedAppLiveData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user