diff --git a/TMessagesProj/build.gradle b/TMessagesProj/build.gradle index b72b9dc69..c1412d276 100644 --- a/TMessagesProj/build.gradle +++ b/TMessagesProj/build.gradle @@ -42,8 +42,8 @@ configurations { compile.exclude module: 'support-v4' } -def okHttpVersion = '4.7.2' -def fcmVersion = '20.2.2' +def okHttpVersion = '4.8.0' +def fcmVersion = '20.2.3' def crashlyticsVersion = '17.1.1' def playCoreVersion = '1.7.3' @@ -63,6 +63,7 @@ buildscript { repositories { + jcenter() maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } @@ -92,17 +93,19 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.6' implementation 'org.osmdroid:osmdroid-android:6.1.6' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okHttpVersion" - implementation 'dnsjava:dnsjava:3.2.1' + implementation 'dnsjava:dnsjava:3.2.2' implementation 'org.dizitart:nitrite:3.4.2' - implementation 'cn.hutool:hutool-core:5.3.8' - implementation 'cn.hutool:hutool-crypto:5.3.8' + implementation 'cn.hutool:hutool-core:5.3.9' + implementation 'cn.hutool:hutool-crypto:5.3.9' implementation 'org.tukaani:xz:1.8' + implementation project(":openpgp-api") + compileOnly files('libs/libv2ray.aar') @@ -271,14 +274,17 @@ android { jniDebuggable false minifyEnabled true shrinkResources true + matchingFallbacks = ['debug'] } release { initWith releaseNoGcm + matchingFallbacks = ['debug'] } foss { initWith release + matchingFallbacks = ['debug'] } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java index c7cc23af0..9ae91cbbc 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java @@ -16,8 +16,11 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.PendingIntent; import android.content.ClipDescription; import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -48,6 +51,7 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextWatcher; import android.text.style.ImageSpan; +import android.util.Log; import android.util.Property; import android.util.TypedValue; import android.view.ActionMode; @@ -88,6 +92,8 @@ import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.customview.widget.ExploreByTouchHelper; import org.jetbrains.annotations.NotNull; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; import org.telegram.messenger.AccountInstance; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; @@ -122,6 +128,8 @@ import org.telegram.ui.GroupStickersActivity; import org.telegram.ui.LaunchActivity; import org.telegram.ui.StickersActivity; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.util.ArrayList; import java.util.HashMap; @@ -129,12 +137,15 @@ import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; import kotlin.Unit; import tw.nekomimi.nekogram.NekoConfig; import tw.nekomimi.nekogram.transtale.TranslateDb; import tw.nekomimi.nekogram.transtale.Translator; import tw.nekomimi.nekogram.transtale.TranslatorKt; import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.PGPUtil; public class ChatActivityEnterView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayout.SizeNotifierFrameLayoutDelegate, StickersAlert.StickersAlertDelegate { @@ -2968,64 +2979,95 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe }); sendPopupLayout.setShowedFromBotton(false); - for (int a = 0; a < 3; a++) { - if (a == 2 && (UserObject.isUserSelf(user) || slowModeTimer > 0 && !isInScheduleMode())) { - continue; - } - int num = a; - ActionBarMenuSubItem cell = new ActionBarMenuSubItem(getContext()); - if (num == 0) { - cell.setTextAndIcon(LocaleController.getString("Translate", R.string.Translate), R.drawable.ic_translate); - } else if (num == 1) { - if (UserObject.isUserSelf(user)) { - cell.setTextAndIcon(LocaleController.getString("SetReminder", R.string.SetReminder), R.drawable.baseline_date_range_24); - } else { - cell.setTextAndIcon(LocaleController.getString("ScheduleMessage", R.string.ScheduleMessage), R.drawable.baseline_date_range_24); - } - } else if (num == 2) { - cell.setTextAndIcon(LocaleController.getString("SendWithoutSound", R.string.SendWithoutSound), R.drawable.input_notify_off); - } - cell.setMinimumWidth(AndroidUtilities.dp(196)); - sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a, 0, 0)); + int chatId; + if (chat != null) { + chatId = chat.id; + } else if (user != null) { + chatId = user.id; + } else { + chatId = -1; + } - int chatId; - if (chat != null) { - chatId = chat.id; - } else if (user != null) { - chatId = user.id; - } else { - chatId = -1; - } + int a = 0; + ActionBarMenuSubItem cell = new ActionBarMenuSubItem(getContext()); + + if (StrUtil.isNotBlank(NekoConfig.openPGPApp)) { + + cell.setTextAndIcon(LocaleController.getString("Sign", R.string.Sign), R.drawable.baseline_vpn_key_24); cell.setOnClickListener(v -> { if (sendPopupWindow != null && sendPopupWindow.isShowing()) { sendPopupWindow.dismiss(); } - if (num == 0) { - translateComment(TranslateDb.getChatLanguage(chatId, TranslatorKt.getCode2Locale(NekoConfig.translateInputLang))); - } - if (num == 1) { - AlertsCreator.createScheduleDatePickerDialog(parentActivity, parentFragment.getDialogId(), this::sendMessageInternal); - } else if (num == 2) { - sendMessageInternal(false, 0); - } + signComment(NekoConfig.openPGPKeyId); + }); cell.setOnLongClickListener(v -> { - if (num == 0) { - Translator.showTargetLangSelect(cell, true, (locale) -> { - if (sendPopupWindow != null && sendPopupWindow.isShowing()) { - sendPopupWindow.dismiss(); - } - translateComment(locale); - TranslateDb.saveChatLanguage(chatId, locale); - return Unit.INSTANCE; - }); - return true; + if (sendPopupWindow != null && sendPopupWindow.isShowing()) { + sendPopupWindow.dismiss(); } - return false; + signComment(1L); + return true; }); + cell.setMinimumWidth(AndroidUtilities.dp(196)); + sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a++, 0, 0)); + + cell = new ActionBarMenuSubItem(getContext()); + } + cell.setTextAndIcon(LocaleController.getString("Translate", R.string.Translate), R.drawable.ic_translate); + cell.setOnClickListener(v -> { + if (sendPopupWindow != null && sendPopupWindow.isShowing()) { + sendPopupWindow.dismiss(); + } + translateComment(TranslateDb.getChatLanguage(chatId, TranslatorKt.getCode2Locale(NekoConfig.translateInputLang))); + }); + ActionBarMenuSubItem finalCell = cell; + cell.setOnLongClickListener(v -> { + Translator.showTargetLangSelect(finalCell, true, (locale) -> { + if (sendPopupWindow != null && sendPopupWindow.isShowing()) { + sendPopupWindow.dismiss(); + } + translateComment(locale); + TranslateDb.saveChatLanguage(chatId, locale); + return Unit.INSTANCE; + }); + return true; + }); + cell.setMinimumWidth(AndroidUtilities.dp(196)); + sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a++, 0, 0)); + + if (!UserObject.isUserSelf(user) && (slowModeTimer == 0 || !isInScheduleMode())) { + + cell = new ActionBarMenuSubItem(getContext()); + if (UserObject.isUserSelf(user)) { + cell.setTextAndIcon(LocaleController.getString("SetReminder", R.string.SetReminder), R.drawable.baseline_date_range_24); + } else { + cell.setTextAndIcon(LocaleController.getString("ScheduleMessage", R.string.ScheduleMessage), R.drawable.baseline_date_range_24); + } + cell.setOnClickListener(v -> { + if (sendPopupWindow != null && sendPopupWindow.isShowing()) { + sendPopupWindow.dismiss(); + } + AlertsCreator.createScheduleDatePickerDialog(parentActivity, parentFragment.getDialogId(), this::sendMessageInternal); + }); + cell.setMinimumWidth(AndroidUtilities.dp(196)); + sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a++, 0, 0)); + + } + + cell = new ActionBarMenuSubItem(getContext()); + cell.setTextAndIcon(LocaleController.getString("SendWithoutSound", R.string.SendWithoutSound), R.drawable.input_notify_off); + cell.setOnClickListener(v -> { + if (sendPopupWindow != null && sendPopupWindow.isShowing()) { + sendPopupWindow.dismiss(); + } + AlertsCreator.createScheduleDatePickerDialog(parentActivity, parentFragment.getDialogId(), this::sendMessageInternal); + }); + cell.setMinimumWidth(AndroidUtilities.dp(196)); + sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a++, 0, 0)); + sendPopupWindow = new ActionBarPopupWindow(sendPopupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT) { @Override public void dismiss() { @@ -3065,6 +3107,71 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe return false; } + private void signComment(long signKeyId) { + + if (parentActivity instanceof LaunchActivity) { + + ((LaunchActivity) parentActivity).callbacks.put(115, result -> { + + long keyId = result.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0L); + if (signKeyId < 1L) NekoConfig.setOpenPGPKeyId(keyId); + + signComment(keyId); + + return Unit.INSTANCE; + + }); + + } + + Intent intent = new Intent(OpenPgpApi.ACTION_CLEARTEXT_SIGN); + + if (signKeyId < 0L) intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, signKeyId); + + ByteArrayInputStream is = IoUtil.toUtf8Stream(messageEditText.getText().toString()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + PGPUtil.post(() -> PGPUtil.api.executeApiAsync(intent, is, os, result -> { + + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + + case OpenPgpApi.RESULT_CODE_SUCCESS: { + + String str = StrUtil.utf8Str(os.toByteArray()); + + if (StrUtil.isNotBlank(str)) messageEditText.setText(str); + + break; + + } + + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { + + PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + try { + parentActivity.startIntentSenderFromChild(parentActivity, pi.getIntentSender(), + 115, null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(OpenPgpApi.TAG, "SendIntentException", e); + } + break; + } + case OpenPgpApi.RESULT_CODE_ERROR: { + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + if (error.getMessage().contains("not found") && signKeyId < 0L) { + NekoConfig.setOpenPGPKeyId(0L); + signComment(0L); + } else { + AlertUtil.showToast(error.getMessage()); + } + break; + } + } + + })); + + } + private void translateComment(Locale target) { TranslateDb db = TranslateDb.forLocale(target); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 9a6e1166e..202aa1840 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -115,9 +115,11 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import kotlin.Unit; import tw.nekomimi.nekogram.NekoConfig; import tw.nekomimi.nekogram.NekoXConfig; import tw.nekomimi.nekogram.NekoXSettingActivity; @@ -2891,6 +2893,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa return rightActionBarLayout; } + public HashMap> callbacks = new HashMap<>(); + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (SharedConfig.passcodeHash.length() != 0 && SharedConfig.lastPauseTime != 0) { @@ -2901,6 +2905,15 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa UserConfig.getInstance(currentAccount).saveConfig(false); } super.onActivityResult(requestCode, resultCode, data); + + if (callbacks.containsKey(requestCode)) { + + callbacks.remove(requestCode).apply(data); + + return; + + } + ThemeEditorView editorView = ThemeEditorView.getInstance(); if (editorView != null) { editorView.onActivityResult(requestCode, resultCode, data); diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java index e3ef746d1..e4e7ebad5 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java @@ -3,6 +3,9 @@ package tw.nekomimi.nekogram; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.BuildVars; @@ -21,7 +24,7 @@ public class NekoConfig { public static final int TITLE_TYPE_ICON = 1; public static final int TITLE_TYPE_MIX = 2; - private static SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("nekoconfig", Activity.MODE_PRIVATE); + public static SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("nekoconfig", Activity.MODE_PRIVATE); public static boolean useIPv6; @@ -109,6 +112,27 @@ public class NekoConfig { public static boolean proxyAutoSwitch; public static boolean usePersianCalender; + public static String openPGPApp; + public static long openPGPKeyId; + + public static String getOpenPGPAppName() { + + if (StrUtil.isNotBlank(openPGPApp)) { + + try { + PackageManager manager = ApplicationLoader.applicationContext.getPackageManager(); + ApplicationInfo info = manager.getApplicationInfo(openPGPApp, PackageManager.GET_META_DATA); + return (String) manager.getApplicationLabel(info); + } catch (PackageManager.NameNotFoundException e) { + openPGPApp = ""; + } + + } + + return LocaleController.getString("None",R.string.None); + + } + public static String formatLang(String name) { if (name == null) { @@ -215,6 +239,8 @@ public class NekoConfig { proxyAutoSwitch = preferences.getBoolean("proxy_auto_switch", false); usePersianCalender = preferences.getBoolean("usePersianCalender", false); + openPGPApp = preferences.getString("openPGPApp",""); + openPGPKeyId = preferences.getLong("openPGPKeyId",0L); } @@ -716,5 +742,17 @@ public class NekoConfig { } + public static void setOpenPGPApp(String packageName) { + + preferences.edit().putString("openPGPApp",openPGPApp = packageName).apply(); + + } + + public static void setOpenPGPKeyId(long keyId) { + + preferences.edit().putLong("openPGPKeyId",openPGPKeyId = keyId).apply(); + + } + } \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoGeneralSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoGeneralSettingsActivity.java index 18dc07a34..369068edd 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoGeneralSettingsActivity.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoGeneralSettingsActivity.java @@ -1,8 +1,14 @@ package tw.nekomimi.nekogram.settings; import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.ResolveInfo; import android.os.Build; +import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -13,6 +19,8 @@ import android.widget.LinearLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ContactsController; import org.telegram.messenger.ImageLoader; @@ -42,6 +50,8 @@ import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.UndoView; import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import cn.hutool.core.util.StrUtil; @@ -49,10 +59,12 @@ import kotlin.Unit; import tw.nekomimi.nekogram.BottomBuilder; import tw.nekomimi.nekogram.EmojiProvider; import tw.nekomimi.nekogram.NekoConfig; +import tw.nekomimi.nekogram.PopupBuilder; import tw.nekomimi.nekogram.transtale.Translator; import tw.nekomimi.nekogram.transtale.TranslatorKt; +import tw.nekomimi.nekogram.utils.AlertUtil; import tw.nekomimi.nekogram.utils.EnvUtil; -import tw.nekomimi.nekogram.PopupBuilder; +import tw.nekomimi.nekogram.utils.PGPUtil; @SuppressLint("RtlHardcoded") public class NekoGeneralSettingsActivity extends BaseFragment { @@ -76,6 +88,12 @@ public class NekoGeneralSettingsActivity extends BaseFragment { private int googleCloudTranslateKeyRow; private int trans2Row; + private int openKeyChainRow; + private int pgpAppRow; + private int emailRow; + private int keyRow; + private int openKeyChain2Row; + private int dialogsRow; private int sortMenuRow; private int dialogs2Row; @@ -455,7 +473,52 @@ public class NekoGeneralSettingsActivity extends BaseFragment { if (view instanceof TextCheckCell) { ((TextCheckCell) view).setChecked(NekoConfig.usePersianCalender); } + } else if (position == pgpAppRow) { + + PopupBuilder builder = new PopupBuilder(view); + + builder.addSubItem(0, LocaleController.getString("None", R.string.None)); + + LinkedList appsMap = new LinkedList<>(); + appsMap.add(""); + + Intent intent = new Intent(OpenPgpApi.SERVICE_INTENT_2); + List resInfo = getParentActivity().getPackageManager().queryIntentServices(intent, 0); + + if (resInfo != null) { + for (ResolveInfo resolveInfo : resInfo) { + if (resolveInfo.serviceInfo == null) { + continue; + } + + String packageName = resolveInfo.serviceInfo.packageName; + String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(getParentActivity().getPackageManager())); + + builder.addSubItem(appsMap.size(), simpleName); + appsMap.add(packageName); + + } + } + + builder.setDelegate((i) -> { + + NekoConfig.setOpenPGPApp(appsMap.get(i)); + NekoConfig.setOpenPGPKeyId(0L); + listAdapter.notifyItemChanged(pgpAppRow); + listAdapter.notifyItemChanged(keyRow); + + if (i > 0) PGPUtil.recreateConnection(); + + }); + + builder.show(); + + } else if (position == keyRow) { + + requestKey(new Intent(OpenPgpApi.ACTION_GET_SIGN_KEY_ID)); + } + }); restartTooltip = new UndoView(context); @@ -465,6 +528,79 @@ public class NekoGeneralSettingsActivity extends BaseFragment { return fragmentView; } + private void requestKey(Intent data) { + + PGPUtil.post(() -> PGPUtil.api.executeApiAsync(data, null, null, result -> { + + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + + case OpenPgpApi.RESULT_CODE_SUCCESS: { + + long keyId = result.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0L); + NekoConfig.setOpenPGPKeyId(keyId); + + listAdapter.notifyItemChanged(keyRow); + + break; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { + + PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + try { + Activity act = (Activity) getParentActivity(); + act.startIntentSenderFromChild( + act, pi.getIntentSender(), + 114, null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(OpenPgpApi.TAG, "SendIntentException", e); + } + break; + } + case OpenPgpApi.RESULT_CODE_ERROR: { + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + AlertUtil.showToast(error.getMessage()); + break; + } + } + + })); + + + } + + @Override + public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { + + if (requestCode == 114 && resultCode == Activity.RESULT_OK) { + + requestKey(data); + + } + + } + + private static class OpenPgpProviderEntry { + private String packageName; + private String simpleName; + private Intent intent; + + OpenPgpProviderEntry(String packageName, String simpleName) { + this.packageName = packageName; + this.simpleName = simpleName; + } + + OpenPgpProviderEntry(String packageName, String simpleName, Intent intent) { + this(packageName, simpleName); + this.intent = intent; + } + + @Override + public String toString() { + return simpleName; + } + } + + private void showSortMenuAlert() { if (getParentActivity() == null) { return; @@ -568,6 +704,12 @@ public class NekoGeneralSettingsActivity extends BaseFragment { googleCloudTranslateKeyRow = rowCount++; trans2Row = rowCount++; + openKeyChainRow = rowCount++; + pgpAppRow = rowCount++; +// emailRow = rowCount++; + keyRow = rowCount++; + openKeyChain2Row = rowCount++; + dialogsRow = rowCount++; sortMenuRow = rowCount++; dialogs2Row = rowCount++; @@ -737,7 +879,11 @@ public class NekoGeneralSettingsActivity extends BaseFragment { default: value = "Unknown"; } - textCell.setTextAndValue(LocaleController.getString("TranslationProvider", R.string.TranslationProvider), value, false); + textCell.setTextAndValue(LocaleController.getString("TranslationProvider", R.string.TranslationProvider), value, true); + } else if (position == pgpAppRow) { + textCell.setTextAndValue(LocaleController.getString("OpenPGPApp", R.string.OpenPGPApp), NekoConfig.getOpenPGPAppName(), true); + } else if (position == keyRow) { + textCell.setTextAndValue(LocaleController.getString("OpenPGPKey", R.string.OpenPGPKey), NekoConfig.openPGPKeyId + "", true); } break; } @@ -816,6 +962,8 @@ public class NekoGeneralSettingsActivity extends BaseFragment { headerCell.setText(LocaleController.getString("DialogsSettings", R.string.DialogsSettings)); } else if (position == privacyRow) { headerCell.setText(LocaleController.getString("PrivacyTitle", R.string.PrivacyTitle)); + } else if (position == openKeyChainRow) { + headerCell.setText(LocaleController.getString("OpenKayChain", R.string.OpenKayChain)); } break; } @@ -868,12 +1016,14 @@ public class NekoGeneralSettingsActivity extends BaseFragment { @Override public int getItemViewType(int position) { if (position == connection2Row || position == dialogs2Row || position == trans2Row || position == privacy2Row || - position == general2Row || position == appearance2Row) { + position == general2Row || position == appearance2Row || position == openKeyChain2Row) { return 1; } else if (position == nameOrderRow || position == sortMenuRow || position == translateToLangRow || position == translateInputToLangRow || - position == translationProviderRow || position == eventTypeRow || position == actionBarDecorationRow) { + position == translationProviderRow || position == eventTypeRow || position == actionBarDecorationRow || + position == pgpAppRow || position == keyRow) { return 2; - } else if (position == connectionRow || position == transRow || position == dialogsRow || position == privacyRow || position == generalRow || position == appearanceRow) { + } else if (position == connectionRow || position == transRow || position == dialogsRow || + position == privacyRow || position == generalRow || position == appearanceRow || position == openKeyChainRow) { return 4; } else if (position == googleCloudTranslateKeyRow || position == cachePathRow) { return 6; diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/utils/PGPUtil.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/utils/PGPUtil.kt new file mode 100644 index 000000000..4638a573f --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/utils/PGPUtil.kt @@ -0,0 +1,75 @@ +package tw.nekomimi.nekogram.utils + +import org.openintents.openpgp.IOpenPgpService2 +import org.openintents.openpgp.util.OpenPgpApi +import org.openintents.openpgp.util.OpenPgpServiceConnection +import org.telegram.messenger.ApplicationLoader +import org.telegram.messenger.FileLog +import tw.nekomimi.nekogram.NekoConfig + +object PGPUtil { + + lateinit var serviceConnection: OpenPgpServiceConnection + lateinit var api: OpenPgpApi + + @JvmStatic + fun recreateConnection() { + + if (::serviceConnection.isInitialized) { + + runCatching { + + serviceConnection.unbindFromService() + + } + + } + + serviceConnection = OpenPgpServiceConnection( + ApplicationLoader.applicationContext, + NekoConfig.openPGPApp + ) + + + } + + @JvmStatic + fun post(runnable: Runnable) { + + if (!::serviceConnection.isInitialized) { + + recreateConnection() + + } + + if (!serviceConnection.isBound) { + + serviceConnection.bindToService(object : OpenPgpServiceConnection.OnBound { + + override fun onBound(service: IOpenPgpService2) { + + api = OpenPgpApi(ApplicationLoader.applicationContext, service) + + runnable.run() + + } + + override fun onError(e: Exception) { + + FileLog.e(e) + + AlertUtil.showToast(e) + + } + + }) + + } else { + + runnable.run() + + } + + } + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/res/values/strings_nekox.xml b/TMessagesProj/src/main/res/values/strings_nekox.xml index c2f6e78a7..cf7c9104b 100644 --- a/TMessagesProj/src/main/res/values/strings_nekox.xml +++ b/TMessagesProj/src/main/res/values/strings_nekox.xml @@ -219,7 +219,12 @@ NekoX FAQ Translate Platform - Translate NekoX Use Persian Calender + OpenPGP Client + OpenPGP App + OpenPGP Key + None + Sign + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 78d85dde3..9fa3f6815 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:4.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72" classpath 'com.google.gms:google-services:4.3.3' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0' diff --git a/openpgp-api/.gitignore b/openpgp-api/.gitignore new file mode 100644 index 000000000..a44cc0f0f --- /dev/null +++ b/openpgp-api/.gitignore @@ -0,0 +1,33 @@ +#Android specific +bin +gen +obj +lint.xml +local.properties +release.properties +ant.properties +*.class +*.apk + +#Gradle +.gradle +build +gradle.properties + +#Maven +target +pom.xml.* + +#Eclipse +.project +.classpath +.settings +.metadata + +#IntelliJ IDEA +.idea +*.iml + +#Lint output +lint-report.html +lint-report_files/* \ No newline at end of file diff --git a/openpgp-api/build.gradle b/openpgp-api/build.gradle new file mode 100644 index 000000000..33506f265 --- /dev/null +++ b/openpgp-api/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + versionCode 9 + versionName '13.0' // API-Version . minor + minSdkVersion 9 + targetSdkVersion 28 + } + + lintOptions { + abortOnError false + } +} \ No newline at end of file diff --git a/openpgp-api/src/main/AndroidManifest.xml b/openpgp-api/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f1f29cf3c --- /dev/null +++ b/openpgp-api/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/openpgp-api/src/main/aidl/org/openintents/openpgp/IOpenPgpService.aidl b/openpgp-api/src/main/aidl/org/openintents/openpgp/IOpenPgpService.aidl new file mode 100644 index 000000000..3689d174b --- /dev/null +++ b/openpgp-api/src/main/aidl/org/openintents/openpgp/IOpenPgpService.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +interface IOpenPgpService { + + /** + * do NOT use this, data returned from the service through "output" may be truncated + * @deprecated + */ + Intent execute(in Intent data, in ParcelFileDescriptor input, in ParcelFileDescriptor output); + +} \ No newline at end of file diff --git a/openpgp-api/src/main/aidl/org/openintents/openpgp/IOpenPgpService2.aidl b/openpgp-api/src/main/aidl/org/openintents/openpgp/IOpenPgpService2.aidl new file mode 100644 index 000000000..8aa4dd2e1 --- /dev/null +++ b/openpgp-api/src/main/aidl/org/openintents/openpgp/IOpenPgpService2.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +interface IOpenPgpService2 { + + /** + * see org.openintents.openpgp.util.OpenPgpApi for documentation + */ + ParcelFileDescriptor createOutputPipe(in int pipeId); + + /** + * see org.openintents.openpgp.util.OpenPgpApi for documentation + */ + Intent execute(in Intent data, in ParcelFileDescriptor input, int pipeId); +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/AutocryptPeerUpdate.java b/openpgp-api/src/main/java/org/openintents/openpgp/AutocryptPeerUpdate.java new file mode 100644 index 000000000..f7357d1c0 --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/AutocryptPeerUpdate.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + + +import java.util.Date; + +import android.os.Parcel; +import android.os.Parcelable; + + +@SuppressWarnings("unused") +public class AutocryptPeerUpdate implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + private static final int PARCELABLE_VERSION = 1; + + + private final byte[] keyData; + private final Date effectiveDate; + private final PreferEncrypt preferEncrypt; + + + private AutocryptPeerUpdate(byte[] keyData, Date effectiveDate, PreferEncrypt preferEncrypt) { + this.keyData = keyData; + this.effectiveDate = effectiveDate; + this.preferEncrypt = preferEncrypt; + } + + private AutocryptPeerUpdate(Parcel source, int version) { + this.keyData = source.createByteArray(); + this.effectiveDate = source.readInt() != 0 ? new Date(source.readLong()) : null; + this.preferEncrypt = PreferEncrypt.values()[source.readInt()]; + } + + + public static AutocryptPeerUpdate createAutocryptPeerUpdate(byte[] keyData, Date timestamp) { + return new AutocryptPeerUpdate(keyData, timestamp, PreferEncrypt.NOPREFERENCE); + } + + public byte[] getKeyData() { + return keyData; + } + + public boolean hasKeyData() { + return keyData != null; + } + + public Date getEffectiveDate() { + return effectiveDate; + } + + public PreferEncrypt getPreferEncrypt() { + return preferEncrypt; + } + + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /* + NOTE: When adding fields in the process of updating this API, make sure to bump + {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + + // version 1 + dest.writeByteArray(keyData); + if (effectiveDate != null) { + dest.writeInt(1); + dest.writeLong(effectiveDate.getTime()); + } else { + dest.writeInt(0); + } + + dest.writeInt(preferEncrypt.ordinal()); + + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public AutocryptPeerUpdate createFromParcel(final Parcel source) { + int version = source.readInt(); // parcelableVersion + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + AutocryptPeerUpdate vr = new AutocryptPeerUpdate(source, version); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return vr; + } + + public AutocryptPeerUpdate[] newArray(final int size) { + return new AutocryptPeerUpdate[size]; + } + }; + + public enum PreferEncrypt { + NOPREFERENCE, MUTUAL + } +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpDecryptionResult.java b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpDecryptionResult.java new file mode 100644 index 000000000..de4f12a4c --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpDecryptionResult.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +import java.util.Arrays; + +import android.os.Parcel; +import android.os.Parcelable; + +public class OpenPgpDecryptionResult implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + public static final int PARCELABLE_VERSION = 2; + + // content not encrypted + public static final int RESULT_NOT_ENCRYPTED = -1; + // insecure! + public static final int RESULT_INSECURE = 0; + // encrypted + public static final int RESULT_ENCRYPTED = 1; + + private final int result; + private final byte[] sessionKey; + private final byte[] decryptedSessionKey; + + public OpenPgpDecryptionResult(int result) { + this.result = result; + this.sessionKey = null; + this.decryptedSessionKey = null; + } + + public OpenPgpDecryptionResult(int result, byte[] sessionKey, byte[] decryptedSessionKey) { + this.result = result; + if ((sessionKey == null) != (decryptedSessionKey == null)) { + throw new AssertionError("sessionkey must be null iff decryptedSessionKey is null"); + } + this.sessionKey = sessionKey; + this.decryptedSessionKey = decryptedSessionKey; + } + + public int getResult() { + return result; + } + + public boolean hasDecryptedSessionKey() { + return sessionKey != null; + } + + public byte[] getSessionKey() { + if (sessionKey == null) { + return null; + } + return Arrays.copyOf(sessionKey, sessionKey.length); + } + + public byte[] getDecryptedSessionKey() { + if (sessionKey == null || decryptedSessionKey == null) { + return null; + } + return Arrays.copyOf(decryptedSessionKey, decryptedSessionKey.length); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /* + NOTE: When adding fields in the process of updating this API, make sure to bump + {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeInt(result); + // version 2 + dest.writeByteArray(sessionKey); + dest.writeByteArray(decryptedSessionKey); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpDecryptionResult createFromParcel(final Parcel source) { + int version = source.readInt(); // parcelableVersion + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + int result = source.readInt(); + byte[] sessionKey = version > 1 ? source.createByteArray() : null; + byte[] decryptedSessionKey = version > 1 ? source.createByteArray() : null; + + OpenPgpDecryptionResult vr = new OpenPgpDecryptionResult(result, sessionKey, decryptedSessionKey); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return vr; + } + + public OpenPgpDecryptionResult[] newArray(final int size) { + return new OpenPgpDecryptionResult[size]; + } + }; + + @Override + public String toString() { + return "\nresult: " + result; + } + +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpError.java b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpError.java new file mode 100644 index 000000000..a149be45f --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpError.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +import android.os.Parcel; +import android.os.Parcelable; + +public class OpenPgpError implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + public static final int PARCELABLE_VERSION = 1; + + // possible values for errorId + public static final int CLIENT_SIDE_ERROR = -1; + public static final int GENERIC_ERROR = 0; + public static final int INCOMPATIBLE_API_VERSIONS = 1; + public static final int NO_OR_WRONG_PASSPHRASE = 2; + public static final int NO_USER_IDS = 3; + public static final int OPPORTUNISTIC_MISSING_KEYS = 4; + + + int errorId; + String message; + + public OpenPgpError() { + } + + public OpenPgpError(int errorId, String message) { + this.errorId = errorId; + this.message = message; + } + + public OpenPgpError(OpenPgpError b) { + this.errorId = b.errorId; + this.message = b.message; + } + + public int getErrorId() { + return errorId; + } + + public void setErrorId(int errorId) { + this.errorId = errorId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /* + NOTE: When adding fields in the process of updating this API, make sure to bump + {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeInt(errorId); + dest.writeString(message); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpError createFromParcel(final Parcel source) { + source.readInt(); // parcelableVersion + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + OpenPgpError error = new OpenPgpError(); + error.errorId = source.readInt(); + error.message = source.readString(); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return error; + } + + public OpenPgpError[] newArray(final int size) { + return new OpenPgpError[size]; + } + }; +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpMetadata.java b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpMetadata.java new file mode 100644 index 000000000..b667e92a7 --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpMetadata.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +import android.os.Parcel; +import android.os.Parcelable; + +public class OpenPgpMetadata implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + public static final int PARCELABLE_VERSION = 2; + + String filename; + String mimeType; + String charset; + long modificationTime; + long originalSize; + + public String getFilename() { + return filename; + } + + public String getMimeType() { + return mimeType; + } + + public long getModificationTime() { + return modificationTime; + } + + public long getOriginalSize() { + return originalSize; + } + + public String getCharset() { + return charset; + } + + public OpenPgpMetadata() { + } + + public OpenPgpMetadata(String filename, String mimeType, long modificationTime, + long originalSize, String charset) { + this.filename = filename; + this.mimeType = mimeType; + this.modificationTime = modificationTime; + this.originalSize = originalSize; + this.charset = charset; + } + + public OpenPgpMetadata(String filename, String mimeType, long modificationTime, + long originalSize) { + this.filename = filename; + this.mimeType = mimeType; + this.modificationTime = modificationTime; + this.originalSize = originalSize; + } + + public OpenPgpMetadata(OpenPgpMetadata b) { + this.filename = b.filename; + this.mimeType = b.mimeType; + this.modificationTime = b.modificationTime; + this.originalSize = b.originalSize; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /* + * NOTE: When adding fields in the process of updating this API, make sure to bump + * {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeString(filename); + dest.writeString(mimeType); + dest.writeLong(modificationTime); + dest.writeLong(originalSize); + // version 2 + dest.writeString(charset); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpMetadata createFromParcel(final Parcel source) { + int version = source.readInt(); // parcelableVersion + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + OpenPgpMetadata vr = new OpenPgpMetadata(); + vr.filename = source.readString(); + vr.mimeType = source.readString(); + vr.modificationTime = source.readLong(); + vr.originalSize = source.readLong(); + if (version >= 2) { + vr.charset = source.readString(); + } + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return vr; + } + + public OpenPgpMetadata[] newArray(final int size) { + return new OpenPgpMetadata[size]; + } + }; + + @Override + public String toString() { + String out = "\nfilename: " + filename; + out += "\nmimeType: " + mimeType; + out += "\nmodificationTime: " + modificationTime; + out += "\noriginalSize: " + originalSize; + out += "\ncharset: " + charset; + return out; + } + +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpSignatureResult.java b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpSignatureResult.java new file mode 100644 index 000000000..80d8e7b23 --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpSignatureResult.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.openintents.openpgp.util.OpenPgpUtils; + +@SuppressWarnings("unused") +public class OpenPgpSignatureResult implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + private static final int PARCELABLE_VERSION = 5; + + // content not signed + public static final int RESULT_NO_SIGNATURE = -1; + // invalid signature! + public static final int RESULT_INVALID_SIGNATURE = 0; + // successfully verified signature, with confirmed key + public static final int RESULT_VALID_KEY_CONFIRMED = 1; + // no key was found for this signature verification + public static final int RESULT_KEY_MISSING = 2; + // successfully verified signature, but with unconfirmed key + public static final int RESULT_VALID_KEY_UNCONFIRMED = 3; + // key has been revoked -> invalid signature! + public static final int RESULT_INVALID_KEY_REVOKED = 4; + // key is expired -> invalid signature! + public static final int RESULT_INVALID_KEY_EXPIRED = 5; + // insecure cryptographic algorithms/protocol -> invalid signature! + public static final int RESULT_INVALID_KEY_INSECURE = 6; + // data wasn't encrypted to recipient intended in signature + public static final int RESULT_INVALID_NOT_INTENDED_RECIPIENT = 7; + + private final int result; + private final long keyId; + private final String primaryUserId; + private final List userIds; + private final List confirmedUserIds; + private final SenderStatusResult senderStatusResult; + private final Date signatureTimestamp; + private final AutocryptPeerResult autocryptPeerentityResult; + + private OpenPgpSignatureResult(int signatureStatus, String signatureUserId, long keyId, + List userIds, List confirmedUserIds, SenderStatusResult senderStatusResult, + Boolean signatureOnly, Date signatureTimestamp, AutocryptPeerResult autocryptPeerentityResult) { + this.result = signatureStatus; + this.primaryUserId = signatureUserId; + this.keyId = keyId; + this.userIds = userIds; + this.confirmedUserIds = confirmedUserIds; + this.senderStatusResult = senderStatusResult; + this.signatureTimestamp = signatureTimestamp; + this.autocryptPeerentityResult = autocryptPeerentityResult; + } + + private OpenPgpSignatureResult(Parcel source, int version) { + this.result = source.readInt(); + // we dropped support for signatureOnly, but need to skip the value for compatibility + source.readByte(); + this.primaryUserId = source.readString(); + this.keyId = source.readLong(); + + if (version > 1) { + this.userIds = source.createStringArrayList(); + } else { + this.userIds = null; + } + // backward compatibility for this exact version + if (version > 2) { + this.senderStatusResult = readEnumWithNullAndFallback( + source, SenderStatusResult.values, SenderStatusResult.UNKNOWN); + this.confirmedUserIds = source.createStringArrayList(); + } else { + this.senderStatusResult = SenderStatusResult.UNKNOWN; + this.confirmedUserIds = null; + } + + if (version > 3) { + this.signatureTimestamp = source.readInt() > 0 ? new Date(source.readLong()) : null; + } else { + this.signatureTimestamp = null; + } + + if (version > 4) { + this.autocryptPeerentityResult = readEnumWithNullAndFallback(source, AutocryptPeerResult.values, null); + } else { + this.autocryptPeerentityResult = null; + } + } + + public int getResult() { + return result; + } + + public SenderStatusResult getSenderStatusResult() { + return senderStatusResult; + } + + public String getPrimaryUserId() { + return primaryUserId; + } + + public List getUserIds() { + return Collections.unmodifiableList(userIds); + } + + public List getConfirmedUserIds() { + return Collections.unmodifiableList(confirmedUserIds); + } + + public long getKeyId() { + return keyId; + } + + public Date getSignatureTimestamp() { + return signatureTimestamp; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /* + NOTE: When adding fields in the process of updating this API, make sure to bump + {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeInt(result); + // signatureOnly is deprecated since version 3. we pass a dummy value for compatibility + dest.writeByte((byte) 0); + dest.writeString(primaryUserId); + dest.writeLong(keyId); + // version 2 + dest.writeStringList(userIds); + // version 3 + writeEnumWithNull(dest, senderStatusResult); + dest.writeStringList(confirmedUserIds); + // version 4 + if (signatureTimestamp != null) { + dest.writeInt(1); + dest.writeLong(signatureTimestamp.getTime()); + } else { + dest.writeInt(0); + } + // version 5 + writeEnumWithNull(dest, autocryptPeerentityResult); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpSignatureResult createFromParcel(final Parcel source) { + int version = source.readInt(); // parcelableVersion + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + OpenPgpSignatureResult vr = new OpenPgpSignatureResult(source, version); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return vr; + } + + public OpenPgpSignatureResult[] newArray(final int size) { + return new OpenPgpSignatureResult[size]; + } + }; + + @Override + public String toString() { + String out = "\nresult: " + result; + out += "\nprimaryUserId: " + primaryUserId; + out += "\nuserIds: " + userIds; + out += "\nkeyId: " + OpenPgpUtils.convertKeyIdToHex(keyId); + return out; + } + + public static OpenPgpSignatureResult createWithValidSignature(int signatureStatus, String primaryUserId, + long keyId, List userIds, List confirmedUserIds, + SenderStatusResult senderStatusResult, Date signatureTimestamp) { + if (signatureStatus == RESULT_NO_SIGNATURE || signatureStatus == RESULT_KEY_MISSING || + signatureStatus == RESULT_INVALID_SIGNATURE) { + throw new IllegalArgumentException("can only use this method for valid types of signatures"); + } + return new OpenPgpSignatureResult(signatureStatus, primaryUserId, keyId, userIds, confirmedUserIds, + senderStatusResult, null, signatureTimestamp, null); + } + + public static OpenPgpSignatureResult createWithNoSignature() { + return new OpenPgpSignatureResult(RESULT_NO_SIGNATURE, null, 0L, null, null, null, null, null, null); + } + + public static OpenPgpSignatureResult createWithKeyMissing(long keyId, Date signatureTimestamp) { + return new OpenPgpSignatureResult(RESULT_KEY_MISSING, null, keyId, null, null, null, null, signatureTimestamp, null); + } + + public static OpenPgpSignatureResult createWithInvalidSignature() { + return new OpenPgpSignatureResult(RESULT_INVALID_SIGNATURE, null, 0L, null, null, null, null, null, null); + } + + @Deprecated + public OpenPgpSignatureResult withSignatureOnlyFlag(boolean signatureOnly) { + return new OpenPgpSignatureResult(result, primaryUserId, keyId, userIds, confirmedUserIds, + senderStatusResult, signatureOnly, signatureTimestamp, autocryptPeerentityResult); + } + + public OpenPgpSignatureResult withAutocryptPeerResult(AutocryptPeerResult autocryptPeerentityResult) { + return new OpenPgpSignatureResult( + result, primaryUserId, keyId, userIds, confirmedUserIds, + senderStatusResult, null, signatureTimestamp, autocryptPeerentityResult); + } + + private static > T readEnumWithNullAndFallback(Parcel source, T[] enumValues, T fallback) { + int valueOrdinal = source.readInt(); + if (valueOrdinal == -1) { + return null; + } + if (valueOrdinal >= enumValues.length) { + return fallback; + } + return enumValues[valueOrdinal]; + } + + private static void writeEnumWithNull(Parcel dest, Enum enumValue) { + if (enumValue == null) { + dest.writeInt(-1); + return; + } + dest.writeInt(enumValue.ordinal()); + } + + public enum SenderStatusResult { + UNKNOWN, USER_ID_CONFIRMED, USER_ID_UNCONFIRMED, USER_ID_MISSING; + public static final SenderStatusResult[] values = values(); + } + + public enum AutocryptPeerResult { + OK, NEW, MISMATCH; + public static final AutocryptPeerResult[] values = values(); + } +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpApi.java b/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpApi.java new file mode 100644 index 000000000..98d38881b --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpApi.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicInteger; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import org.openintents.openpgp.IOpenPgpService2; +import org.openintents.openpgp.OpenPgpError; + +public class OpenPgpApi { + + public static final String TAG = "OpenPgp API"; + + public static final String SERVICE_INTENT_2 = "org.openintents.openpgp.IOpenPgpService2"; + + /** + * see CHANGELOG.md + */ + public static final int API_VERSION = 11; + + /** + * General extras + * -------------- + * + * required extras: + * int EXTRA_API_VERSION (always required) + * + * returned extras: + * int RESULT_CODE (RESULT_CODE_ERROR, RESULT_CODE_SUCCESS or RESULT_CODE_USER_INTERACTION_REQUIRED) + * OpenPgpError RESULT_ERROR (if RESULT_CODE == RESULT_CODE_ERROR) + * PendingIntent RESULT_INTENT (if RESULT_CODE == RESULT_CODE_USER_INTERACTION_REQUIRED) + */ + + /** + * This action performs no operation, but can be used to check if the App has permission + * to access the API in general, returning a user interaction PendingIntent otherwise. + * This can be used to trigger the permission dialog explicitly. + * + * This action uses no extras. + */ + public static final String ACTION_CHECK_PERMISSION = "org.openintents.openpgp.action.CHECK_PERMISSION"; + + @Deprecated + public static final String ACTION_SIGN = "org.openintents.openpgp.action.SIGN"; + + /** + * Sign text resulting in a cleartext signature + * Some magic pre-processing of the text is done to convert it to a format usable for + * cleartext signatures per RFC 4880 before the text is actually signed: + * - end cleartext with newline + * - remove whitespaces on line endings + * + * required extras: + * long EXTRA_SIGN_KEY_ID (key id of signing key) + * + * optional extras: + * char[] EXTRA_PASSPHRASE (key passphrase) + */ + public static final String ACTION_CLEARTEXT_SIGN = "org.openintents.openpgp.action.CLEARTEXT_SIGN"; + + /** + * Sign text or binary data resulting in a detached signature. + * No OutputStream necessary for ACTION_DETACHED_SIGN (No magic pre-processing like in ACTION_CLEARTEXT_SIGN)! + * The detached signature is returned separately in RESULT_DETACHED_SIGNATURE. + * + * required extras: + * long EXTRA_SIGN_KEY_ID (key id of signing key) + * + * optional extras: + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for detached signature) + * char[] EXTRA_PASSPHRASE (key passphrase) + * + * returned extras: + * byte[] RESULT_DETACHED_SIGNATURE + * String RESULT_SIGNATURE_MICALG (contains the name of the used signature algorithm as a string) + */ + public static final String ACTION_DETACHED_SIGN = "org.openintents.openpgp.action.DETACHED_SIGN"; + + /** + * Encrypt + * + * required extras: + * String[] EXTRA_USER_IDS (=emails of recipients, if more than one key has a user_id, a PendingIntent is returned via RESULT_INTENT) + * or + * long[] EXTRA_KEY_IDS + * + * optional extras: + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) + * char[] EXTRA_PASSPHRASE (key passphrase) + * String EXTRA_ORIGINAL_FILENAME (original filename to be encrypted as metadata) + * boolean EXTRA_ENABLE_COMPRESSION (enable ZLIB compression, default ist true) + */ + public static final String ACTION_ENCRYPT = "org.openintents.openpgp.action.ENCRYPT"; + + /** + * Sign and encrypt + * + * required extras: + * String[] EXTRA_USER_IDS (=emails of recipients, if more than one key has a user_id, a PendingIntent is returned via RESULT_INTENT) + * or + * long[] EXTRA_KEY_IDS + * + * optional extras: + * long EXTRA_SIGN_KEY_ID (key id of signing key) + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) + * char[] EXTRA_PASSPHRASE (key passphrase) + * String EXTRA_ORIGINAL_FILENAME (original filename to be encrypted as metadata) + * boolean EXTRA_ENABLE_COMPRESSION (enable ZLIB compression, default ist true) + */ + public static final String ACTION_SIGN_AND_ENCRYPT = "org.openintents.openpgp.action.SIGN_AND_ENCRYPT"; + + public static final String ACTION_QUERY_AUTOCRYPT_STATUS = "org.openintents.openpgp.action.QUERY_AUTOCRYPT_STATUS"; + + /** + * Decrypts and verifies given input stream. This methods handles encrypted-only, signed-and-encrypted, + * and also signed-only input. + * OutputStream is optional, e.g., for verifying detached signatures! + * + * If OpenPgpSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_KEY_MISSING + * in addition a PendingIntent is returned via RESULT_INTENT to download missing keys. + * On all other status, in addition a PendingIntent is returned via RESULT_INTENT to open + * the key view in OpenKeychain. + * + * optional extras: + * byte[] EXTRA_DETACHED_SIGNATURE (detached signature) + * + * returned extras: + * OpenPgpSignatureResult RESULT_SIGNATURE + * OpenPgpDecryptionResult RESULT_DECRYPTION + * OpenPgpDecryptMetadata RESULT_METADATA + * String RESULT_CHARSET (charset which was specified in the headers of ascii armored input, if any) + */ + public static final String ACTION_DECRYPT_VERIFY = "org.openintents.openpgp.action.DECRYPT_VERIFY"; + + /** + * Decrypts the header of an encrypted file to retrieve metadata such as original filename. + * + * This does not decrypt the actual content of the file. + * + * returned extras: + * OpenPgpDecryptMetadata RESULT_METADATA + * String RESULT_CHARSET (charset which was specified in the headers of ascii armored input, if any) + */ + public static final String ACTION_DECRYPT_METADATA = "org.openintents.openpgp.action.DECRYPT_METADATA"; + + /** + * Select key id for signing + * + * optional extras: + * String EXTRA_USER_ID + * + * returned extras: + * long EXTRA_SIGN_KEY_ID + */ + public static final String ACTION_GET_SIGN_KEY_ID = "org.openintents.openpgp.action.GET_SIGN_KEY_ID"; + public static final String ACTION_GET_SIGN_KEY_ID_LEGACY = "org.openintents.openpgp.action.GET_SIGN_KEY_ID_LEGACY"; + + /** + * Get key ids based on given user ids (=emails) + * + * required extras: + * String[] EXTRA_USER_IDS + * + * returned extras: + * long[] RESULT_KEY_IDS + */ + public static final String ACTION_GET_KEY_IDS = "org.openintents.openpgp.action.GET_KEY_IDS"; + + /** + * This action returns RESULT_CODE_SUCCESS if the OpenPGP Provider already has the key + * corresponding to the given key id in its database. + * + * It returns RESULT_CODE_USER_INTERACTION_REQUIRED if the Provider does not have the key. + * The PendingIntent from RESULT_INTENT can be used to retrieve those from a keyserver. + * + * If an Output stream has been defined the whole public key is returned. + * required extras: + * long EXTRA_KEY_ID + * + * optional extras: + * String EXTRA_REQUEST_ASCII_ARMOR (request that the returned key is encoded in ASCII Armor) + */ + public static final String ACTION_GET_KEY = "org.openintents.openpgp.action.GET_KEY"; + + /** + * Backup all keys given by EXTRA_KEY_IDS and if requested their secret parts. + * The encrypted backup will be written to the OutputStream. + * The client app has no access to the backup code used to encrypt the backup! + * This operation always requires user interaction with RESULT_CODE_USER_INTERACTION_REQUIRED! + * + * required extras: + * long[] EXTRA_KEY_IDS (keys that should be included in the backup) + * boolean EXTRA_BACKUP_SECRET (also backup secret keys) + */ + public static final String ACTION_BACKUP = "org.openintents.openpgp.action.BACKUP"; + + public static final String ACTION_UPDATE_AUTOCRYPT_PEER = "org.openintents.openpgp.action.UPDATE_AUTOCRYPT_PEER"; + + /* Intent extras */ + public static final String EXTRA_API_VERSION = "api_version"; + + // ACTION_DETACHED_SIGN, ENCRYPT, SIGN_AND_ENCRYPT, DECRYPT_VERIFY + // request ASCII Armor for output + // OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53) + public static final String EXTRA_REQUEST_ASCII_ARMOR = "ascii_armor"; + + // ACTION_DETACHED_SIGN + public static final String RESULT_DETACHED_SIGNATURE = "detached_signature"; + public static final String RESULT_SIGNATURE_MICALG = "signature_micalg"; + + // ENCRYPT, SIGN_AND_ENCRYPT, QUERY_AUTOCRYPT_STATUS + public static final String EXTRA_USER_IDS = "user_ids"; + public static final String EXTRA_KEY_IDS = "key_ids"; + public static final String EXTRA_KEY_IDS_SELECTED = "key_ids_selected"; + public static final String EXTRA_SIGN_KEY_ID = "sign_key_id"; + + public static final String RESULT_KEYS_CONFIRMED = "keys_confirmed"; + public static final String RESULT_AUTOCRYPT_STATUS = "autocrypt_status"; + public static final int AUTOCRYPT_STATUS_UNAVAILABLE = 0; + public static final int AUTOCRYPT_STATUS_DISCOURAGE = 1; + public static final int AUTOCRYPT_STATUS_AVAILABLE = 2; + public static final int AUTOCRYPT_STATUS_MUTUAL = 3; + + // optional extras: + public static final String EXTRA_PASSPHRASE = "passphrase"; + public static final String EXTRA_ORIGINAL_FILENAME = "original_filename"; + public static final String EXTRA_ENABLE_COMPRESSION = "enable_compression"; + public static final String EXTRA_OPPORTUNISTIC_ENCRYPTION = "opportunistic"; + public static final String EXTRA_CUSTOM_HEADERS = "custom_headers"; + + // GET_SIGN_KEY_ID + public static final String EXTRA_USER_ID = "user_id"; + public static final String EXTRA_PRESELECT_KEY_ID = "preselect_key_id"; + public static final String EXTRA_SHOW_AUTOCRYPT_HINT = "show_autocrypt_hint"; + + public static final String RESULT_SIGN_KEY_ID = "sign_key_id"; + public static final String RESULT_PRIMARY_USER_ID = "primary_user_id"; + public static final String RESULT_KEY_CREATION_TIME = "key_creation_time"; + + // GET_KEY + public static final String EXTRA_KEY_ID = "key_id"; + public static final String EXTRA_MINIMIZE = "minimize"; + public static final String EXTRA_MINIMIZE_USER_ID = "minimize_user_id"; + public static final String RESULT_KEY_IDS = "key_ids"; + + // BACKUP + public static final String EXTRA_BACKUP_SECRET = "backup_secret"; + + public static final String ACTION_AUTOCRYPT_KEY_TRANSFER = "autocrypt_key_transfer"; + + /* Service Intent returns */ + public static final String RESULT_CODE = "result_code"; + + // get actual error object from RESULT_ERROR + public static final int RESULT_CODE_ERROR = 0; + // success! + public static final int RESULT_CODE_SUCCESS = 1; + // get PendingIntent from RESULT_INTENT, start PendingIntent with startIntentSenderForResult, + // and execute service method again in onActivityResult + public static final int RESULT_CODE_USER_INTERACTION_REQUIRED = 2; + + public static final String RESULT_ERROR = "error"; + public static final String RESULT_INTENT = "intent"; + + // DECRYPT_VERIFY + public static final String EXTRA_DETACHED_SIGNATURE = "detached_signature"; + public static final String EXTRA_PROGRESS_MESSENGER = "progress_messenger"; + public static final String EXTRA_DATA_LENGTH = "data_length"; + public static final String EXTRA_DECRYPTION_RESULT = "decryption_result"; + public static final String EXTRA_SENDER_ADDRESS = "sender_address"; + public static final String EXTRA_SUPPORT_OVERRIDE_CRYPTO_WARNING = "support_override_crpto_warning"; + public static final String EXTRA_AUTOCRYPT_PEER_ID = "autocrypt_peer_id"; + public static final String EXTRA_AUTOCRYPT_PEER_UPDATE = "autocrypt_peer_update"; + public static final String EXTRA_AUTOCRYPT_PEER_GOSSIP_UPDATES = "autocrypt_peer_gossip_updates"; + public static final String RESULT_SIGNATURE = "signature"; + public static final String RESULT_DECRYPTION = "decryption"; + public static final String RESULT_METADATA = "metadata"; + public static final String RESULT_INSECURE_DETAIL_INTENT = "insecure_detail_intent"; + public static final String RESULT_OVERRIDE_CRYPTO_WARNING = "override_crypto_warning"; + // This will be the charset which was specified in the headers of ascii armored input, if any + public static final String RESULT_CHARSET = "charset"; + + // INTERNAL, must not be used + public static final String EXTRA_CALL_UUID1 = "call_uuid1"; + public static final String EXTRA_CALL_UUID2 = "call_uuid2"; + + IOpenPgpService2 mService; + Context mContext; + final AtomicInteger mPipeIdGen = new AtomicInteger(); + + public OpenPgpApi(Context context, IOpenPgpService2 service) { + this.mContext = context; + this.mService = service; + } + + public interface IOpenPgpCallback { + void onReturn(final Intent result); + } + + private class OpenPgpAsyncTask extends AsyncTask { + Intent data; + InputStream is; + OutputStream os; + IOpenPgpCallback callback; + + private OpenPgpAsyncTask(Intent data, InputStream is, OutputStream os, IOpenPgpCallback callback) { + this.data = data; + this.is = is; + this.os = os; + this.callback = callback; + } + + @Override + protected Intent doInBackground(Void... unused) { + return executeApi(data, is, os); + } + + protected void onPostExecute(Intent result) { + callback.onReturn(result); + } + + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void executeApiAsync(Intent data, InputStream is, OutputStream os, IOpenPgpCallback callback) { + OpenPgpAsyncTask task = new OpenPgpAsyncTask(data, is, os, callback); + + // don't serialize async tasks! + // http://commonsware.com/blog/2012/04/20/asynctask-threading-regression-confirmed.html + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } else { + task.execute((Void[]) null); + } + } + + public Intent executeApi(Intent data, InputStream is, OutputStream os) { + ParcelFileDescriptor input = null; + try { + if (is != null) { + input = ParcelFileDescriptorUtil.pipeFrom(is); + } + + return executeApi(data, input, os); + } catch (Exception e) { + Log.e(OpenPgpApi.TAG, "Exception in executeApi call", e); + Intent result = new Intent(); + result.putExtra(RESULT_CODE, RESULT_CODE_ERROR); + result.putExtra(RESULT_ERROR, + new OpenPgpError(OpenPgpError.CLIENT_SIDE_ERROR, e.getMessage())); + return result; + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + Log.e(OpenPgpApi.TAG, "IOException when closing ParcelFileDescriptor!", e); + } + } + } + } + + /** + * InputStream and OutputStreams are always closed after operating on them! + */ + public Intent executeApi(Intent data, ParcelFileDescriptor input, OutputStream os) { + ParcelFileDescriptor output = null; + try { + // always send version from client + data.putExtra(EXTRA_API_VERSION, OpenPgpApi.API_VERSION); + + Intent result; + + Thread pumpThread = null; + int outputPipeId = 0; + + if (os != null) { + outputPipeId = mPipeIdGen.incrementAndGet(); + output = mService.createOutputPipe(outputPipeId); + pumpThread = ParcelFileDescriptorUtil.pipeTo(os, output); + } + + // blocks until result is ready + result = mService.execute(data, input, outputPipeId); + + // set class loader to current context to allow unparcelling + // of OpenPgpError and OpenPgpSignatureResult + // http://stackoverflow.com/a/3806769 + result.setExtrasClassLoader(mContext.getClassLoader()); + + //wait for ALL data being pumped from remote side + if (pumpThread != null) { + pumpThread.join(); + } + + return result; + } catch (Exception e) { + Log.e(OpenPgpApi.TAG, "Exception in executeApi call", e); + Intent result = new Intent(); + result.putExtra(RESULT_CODE, RESULT_CODE_ERROR); + result.putExtra(RESULT_ERROR, + new OpenPgpError(OpenPgpError.CLIENT_SIDE_ERROR, e.getMessage())); + return result; + } finally { + // close() is required to halt the TransferThread + if (output != null) { + try { + output.close(); + } catch (IOException e) { + Log.e(OpenPgpApi.TAG, "IOException when closing ParcelFileDescriptor!", e); + } + } + } + } + +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpServiceConnection.java b/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpServiceConnection.java new file mode 100644 index 000000000..38f6aac8d --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpServiceConnection.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +import org.openintents.openpgp.IOpenPgpService2; + +public class OpenPgpServiceConnection { + + // callback interface + public interface OnBound { + void onBound(IOpenPgpService2 service); + + void onError(Exception e); + } + + private Context mApplicationContext; + + private IOpenPgpService2 mService; + private String mProviderPackageName; + + private OnBound mOnBoundListener; + + /** + * Create new connection + * + * @param context + * @param providerPackageName specify package name of OpenPGP provider, + * e.g., "org.sufficientlysecure.keychain" + */ + public OpenPgpServiceConnection(Context context, String providerPackageName) { + this.mApplicationContext = context.getApplicationContext(); + this.mProviderPackageName = providerPackageName; + } + + /** + * Create new connection with callback + * + * @param context + * @param providerPackageName specify package name of OpenPGP provider, + * e.g., "org.sufficientlysecure.keychain" + * @param onBoundListener callback, executed when connection to service has been established + */ + public OpenPgpServiceConnection(Context context, String providerPackageName, + OnBound onBoundListener) { + this(context, providerPackageName); + this.mOnBoundListener = onBoundListener; + } + + public IOpenPgpService2 getService() { + return mService; + } + + public boolean isBound() { + return (mService != null); + } + + private ServiceConnection mServiceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IOpenPgpService2.Stub.asInterface(service); + if (mOnBoundListener != null) { + mOnBoundListener.onBound(mService); + } + } + + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + /** + * If not already bound, bind to service! + * + * @return + */ + public void bindToService(OnBound onBoundListener) { + if (onBoundListener != null) { + mOnBoundListener = onBoundListener; + } + // if not already bound... + if (mService == null) { + try { + Intent serviceIntent = new Intent(OpenPgpApi.SERVICE_INTENT_2); + // NOTE: setPackage is very important to restrict the intent to this provider only! + serviceIntent.setPackage(mProviderPackageName); + boolean connect = mApplicationContext.bindService(serviceIntent, mServiceConnection, + Context.BIND_AUTO_CREATE); + if (!connect) { + throw new Exception("bindService failed"); + } + } catch (Exception e) { + if (mOnBoundListener != null) { + mOnBoundListener.onError(e); + } + } + } else { + // already bound, but also inform client about it with callback + if (mOnBoundListener != null) { + mOnBoundListener.onBound(mService); + } + } + } + + public void unbindFromService() { + mApplicationContext.unbindService(mServiceConnection); + } + +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpUtils.java b/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpUtils.java new file mode 100644 index 000000000..2c6d7880c --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpUtils.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import java.io.Serializable; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; + +public class OpenPgpUtils { + + public static final Pattern PGP_MESSAGE = Pattern.compile( + ".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", + Pattern.DOTALL); + + public static final Pattern PGP_SIGNED_MESSAGE = Pattern.compile( + ".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*", + Pattern.DOTALL); + + public static final int PARSE_RESULT_NO_PGP = -1; + public static final int PARSE_RESULT_MESSAGE = 0; + public static final int PARSE_RESULT_SIGNED_MESSAGE = 1; + + public static int parseMessage(String message) { + Matcher matcherSigned = PGP_SIGNED_MESSAGE.matcher(message); + Matcher matcherMessage = PGP_MESSAGE.matcher(message); + + if (matcherMessage.matches()) { + return PARSE_RESULT_MESSAGE; + } else if (matcherSigned.matches()) { + return PARSE_RESULT_SIGNED_MESSAGE; + } else { + return PARSE_RESULT_NO_PGP; + } + } + + public static boolean isAvailable(Context context) { + Intent intent = new Intent(OpenPgpApi.SERVICE_INTENT_2); + List resInfo = context.getPackageManager().queryIntentServices(intent, 0); + return !resInfo.isEmpty(); + } + + public static String convertKeyIdToHex(long keyId) { + return "0x" + convertKeyIdToHex32bit(keyId >> 32) + convertKeyIdToHex32bit(keyId); + } + + private static String convertKeyIdToHex32bit(long keyId) { + String hexString = Long.toHexString(keyId & 0xffffffffL).toLowerCase(Locale.ENGLISH); + while (hexString.length() < 8) { + hexString = "0" + hexString; + } + return hexString; + } + + + private static final Pattern USER_ID_PATTERN = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$"); + + private static final Pattern EMAIL_PATTERN = Pattern.compile("^\"]*@[^<>\"]*\\.[^<>\"]*)\"?>?$"); + + /** + * Splits userId string into naming part, email part, and comment part. + * See SplitUserIdTest for examples. + */ + public static UserId splitUserId(final String userId) { + if (!TextUtils.isEmpty(userId)) { + final Matcher matcher = USER_ID_PATTERN.matcher(userId); + if (matcher.matches()) { + String name = matcher.group(1).isEmpty() ? null : matcher.group(1); + String comment = matcher.group(2); + String email = matcher.group(3); + if (email != null && name != null) { + final Matcher emailMatcher = EMAIL_PATTERN.matcher(name); + if (emailMatcher.matches() && email.equals(emailMatcher.group(1))) { + email = emailMatcher.group(1); + name = null; + } + } + if (email == null && name != null) { + final Matcher emailMatcher = EMAIL_PATTERN.matcher(name); + if (emailMatcher.matches()) { + email = emailMatcher.group(1); + name = null; + } + } + return new UserId(name, email, comment); + } + } + return new UserId(null, null, null); + } + + /** + * Returns a composed user id. Returns null if name, email and comment are empty. + */ + public static String createUserId(UserId userId) { + StringBuilder userIdBuilder = new StringBuilder(); + if (!TextUtils.isEmpty(userId.name)) { + userIdBuilder.append(userId.name); + } + if (!TextUtils.isEmpty(userId.comment)) { + userIdBuilder.append(" ("); + userIdBuilder.append(userId.comment); + userIdBuilder.append(")"); + } + if (!TextUtils.isEmpty(userId.email)) { + userIdBuilder.append(" <"); + userIdBuilder.append(userId.email); + userIdBuilder.append(">"); + } + return userIdBuilder.length() == 0 ? null : userIdBuilder.toString(); + } + + public static class UserId implements Serializable { + public final String name; + public final String email; + public final String comment; + + public UserId(String name, String email, String comment) { + this.name = name; + this.email = email; + this.comment = comment; + } + } +} diff --git a/openpgp-api/src/main/java/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java b/openpgp-api/src/main/java/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java new file mode 100644 index 000000000..931ed845e --- /dev/null +++ b/openpgp-api/src/main/java/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann + * 2013 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ParcelFileDescriptorUtil { + + public static ParcelFileDescriptor pipeFrom(InputStream inputStream) + throws IOException { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + ParcelFileDescriptor readSide = pipe[0]; + ParcelFileDescriptor writeSide = pipe[1]; + + new TransferThread(inputStream, new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) + .start(); + + return readSide; + } + + + public static TransferThread pipeTo(OutputStream outputStream, ParcelFileDescriptor output) + throws IOException { + + TransferThread t = new TransferThread(new ParcelFileDescriptor.AutoCloseInputStream(output), outputStream); + + t.start(); + return t; + } + + + static class TransferThread extends Thread { + final InputStream mIn; + final OutputStream mOut; + + TransferThread(InputStream in, OutputStream out) { + super("IPC Transfer Thread"); + mIn = in; + mOut = out; + setDaemon(true); + } + + @Override + public void run() { + byte[] buf = new byte[4096]; + int len; + + try { + while ((len = mIn.read(buf)) > 0) { + mOut.write(buf, 0, len); + } + } catch (IOException e) { + Log.e(OpenPgpApi.TAG, "IOException when writing to out", e); + } finally { + try { + mIn.close(); + } catch (IOException ignored) { + } + try { + mOut.close(); + } catch (IOException ignored) { + } + } + } + } + +} diff --git a/settings.gradle b/settings.gradle index 498a68eb0..bc12b1e03 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include ':TMessagesProj' +include ':openpgp-api' include ':ss-rust' include ':ssr-libev' //include ':relaybaton' \ No newline at end of file