新增:发送通道`电子邮箱`支持`S/MIME`或`OpenPGP`加密 #417

优化:以`Base64`形式保存证书(同时兼容`文件路径`形式) #437
pull/473/head
pppscn 2 months ago
parent 40ee077ea7
commit a53fa6db12

@ -24,6 +24,7 @@ import com.idormy.sms.forwarder.database.viewmodel.SenderViewModel
import com.idormy.sms.forwarder.databinding.FragmentSendersEmailBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.setting.EmailSetting
import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
import com.idormy.sms.forwarder.utils.KEY_SENDER_CLONE
@ -51,6 +52,7 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.pgpainless.PGPainless
import org.pgpainless.key.info.KeyRingInfo
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.security.KeyStore
@ -377,8 +379,14 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
try {
// 判断是否有效的PKCS12私钥证书
val fileInputStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(FileInputStream(cert.first), cert.second.toCharArray())
keyStore.load(fileInputStream, cert.second.toCharArray())
val alias = keyStore.aliases().nextElement()
val recipientPublicKey = keyStore.getCertificate(alias) as X509Certificate
Log.d(TAG, "PKCS12 Certificate: $recipientPublicKey")
@ -391,8 +399,13 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
try {
// 判断是否有效的X.509公钥证书
val fileInputStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val certFactory = CertificateFactory.getInstance("X.509")
val fileInputStream = FileInputStream(cert.first)
val recipientPublicKey = certFactory.generateCertificate(fileInputStream) as X509Certificate
Log.d(TAG, "X.509 Certificate: $recipientPublicKey")
} catch (e: Exception) {
@ -408,7 +421,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
try {
//从私钥证书文件提取公钥
val recipientPrivateKeyStream = FileInputStream(cert.first)
val recipientPrivateKeyStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val recipientPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(recipientPrivateKeyStream)
val recipientPGPPublicKeyRing = PGPainless.extractCertificate(recipientPGPSecretKeyRing!!)
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing)
@ -422,7 +440,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
try {
//从证书文件提取公钥
val recipientPublicKeyStream = FileInputStream(cert.first)
val recipientPublicKeyStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val recipientPGPPublicKeyRing = PGPainless.readKeyRing().publicKeyRing(recipientPublicKeyStream)
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing!!)
Log.d(TAG, "recipientPGPPublicKeyRing: $keyInfo")
@ -448,7 +471,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
val keystore = binding!!.etSenderKeystore.text.toString().trim()
val password = binding!!.etSenderPassword.text.toString().trim()
if (keystore.isNotEmpty()) {
val senderPrivateKeyStream = FileInputStream(keystore)
val senderPrivateKeyStream = if (keystore.startsWith("/")) {
FileInputStream(keystore)
} else {
val decodedBytes = Base64.decode(keystore)
ByteArrayInputStream(decodedBytes)
}
if (senderPrivateKeyStream.available() <= 0) {
throw Exception(getString(R.string.invalid_sender_keystore))
}
@ -567,12 +595,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
return
}
MaterialDialog.Builder(requireContext())
.title(getString(R.string.keystore_path))
.title(getString(R.string.keystore_base64))
.content(String.format(getString(R.string.root_directory), downloadPath))
.items(fileList)
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
val webPath = "$downloadPath/$text"
etKeyStore.setText(webPath)
etKeyStore.setText(convertCertToBase64String(webPath))
true // allow selection
}
.positiveText(R.string.select)
@ -614,6 +642,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
return supportedExtensions.any { it.equals(file.extension, ignoreCase = true) }
}
private fun convertCertToBase64String(pfxFilePath: String): String {
val pfxInputStream = FileInputStream(pfxFilePath)
val pfxBytes = pfxInputStream.readBytes()
return Base64.encode(pfxBytes)
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()

@ -4,6 +4,7 @@ import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.setting.EmailSetting
import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.SendUtils
import com.idormy.sms.forwarder.utils.SettingUtils
@ -16,6 +17,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.pgpainless.PGPainless
import org.pgpainless.key.info.KeyRingInfo
import java.io.ByteArrayInputStream
import java.io.FileInputStream
import java.security.KeyStore
import java.security.PrivateKey
@ -166,8 +168,13 @@ class EmailUtils {
var senderPGPSecretKeyPassword = ""
if (!setting.keystore.isNullOrEmpty() && !setting.password.isNullOrEmpty()) {
val keystoreStream = FileInputStream(setting.keystore)
try {
val keystoreStream = if (setting.keystore!!.startsWith("/")) {
FileInputStream(setting.keystore)
} else {
val decodedBytes = Base64.decode(setting.keystore!!)
ByteArrayInputStream(decodedBytes)
}
when (setting.encryptionProtocol) {
"S/MIME" -> {
val keystorePassword = setting.password.toString()
@ -206,15 +213,21 @@ class EmailUtils {
//逐一发送加密邮件
val recipientsWithoutCert = mutableListOf<String>()
setting.recipients.forEach { (email, cert) ->
val keystorePath = cert.first
val keystoreBase64 = cert.first
val keystorePassword = cert.second
var recipientX509Cert: X509Certificate? = null
var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null
try {
when {
//从私钥证书文件提取公钥
keystorePath.isNotEmpty() && keystorePassword.isNotEmpty() -> {
val keystoreStream = FileInputStream(keystorePath)
keystoreBase64.isNotEmpty() && keystorePassword.isNotEmpty() -> {
val keystoreStream = if (keystoreBase64.startsWith("/")) {
FileInputStream(keystoreBase64)
} else {
val decodedBytes = Base64.decode(keystoreBase64)
ByteArrayInputStream(decodedBytes)
}
when (setting.encryptionProtocol) {
"S/MIME" -> {
val keyStore = KeyStore.getInstance("PKCS12")
@ -235,12 +248,18 @@ class EmailUtils {
}
//从证书文件提取公钥
keystorePath.isNotEmpty() && keystorePassword.isEmpty() -> {
val keystoreStream = FileInputStream(keystorePath)
keystoreBase64.isNotEmpty() && keystorePassword.isEmpty() -> {
val keystoreStream = if (keystoreBase64.startsWith("/")) {
FileInputStream(keystoreBase64)
} else {
val decodedBytes = Base64.decode(keystoreBase64)
ByteArrayInputStream(decodedBytes)
}
when (setting.encryptionProtocol) {
"S/MIME" -> {
val certFactory = CertificateFactory.getInstance("X.509")
recipientX509Cert = certFactory.generateCertificate(FileInputStream(keystorePath)) as X509Certificate
recipientX509Cert = certFactory.generateCertificate(FileInputStream(keystoreBase64)) as X509Certificate
}
"OpenPGP" -> {

@ -368,7 +368,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/keystore_path"
android:text="@string/keystore_base64"
android:textSize="@dimen/text_size_small"
android:textStyle="bold" />
@ -378,10 +378,13 @@
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/keystore_path_tips"
android:hint="@string/keystore_base64_tips"
android:importantForAutofill="no"
android:singleLine="true"
android:textSize="@dimen/text_size_small"
android:inputType="textMultiLine"
android:maxLines="5"
android:minLines="2"
android:scrollbars="vertical"
android:textSize="@dimen/text_size_mini"
app:met_clearButton="true" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton

@ -63,7 +63,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/keystore_path"
android:text="@string/keystore_base64"
android:textSize="@dimen/text_size_small"
android:textStyle="bold" />
@ -73,10 +73,13 @@
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/keystore_path_tips"
android:hint="@string/keystore_base64_tips"
android:importantForAutofill="no"
android:singleLine="true"
android:textSize="@dimen/text_size_small"
android:inputType="textMultiLine"
android:maxLines="5"
android:minLines="2"
android:scrollbars="vertical"
android:textSize="@dimen/text_size_mini"
app:met_clearButton="true" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton

@ -235,8 +235,8 @@
<string name="sender_openpgp_keystore">Sender OpenPGP Cert. (Opt.)</string>
<string name="invalid_sender_keystore">Invalid Sender Signing Private Key</string>
<string name="recipient_email">Recipient</string>
<string name="keystore_path">Cert. Path</string>
<string name="keystore_path_tips">Opt., Copy keystore to the Download dir</string>
<string name="keystore_base64">Specified Cert.</string>
<string name="keystore_base64_tips">Opt., Copy keystore to the Download dir</string>
<string name="keystore_password">Cert. Pwd.</string>
<string name="keystore_password_tips">Import password for `Private key`</string>
<string name="email_title">Email Title</string>

@ -236,8 +236,8 @@
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥可选</string>
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
<string name="recipient_email">收件人邮箱</string>
<string name="keystore_path">证书路径</string>
<string name="keystore_path_tips">可选,下载证书文件到 Download 目录</string>
<string name="keystore_base64">指定证书</string>
<string name="keystore_base64_tips">可选,下载证书文件到 Download 目录</string>
<string name="keystore_password">证书密码</string>
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
<string name="email_title">邮件主题</string>

@ -236,8 +236,8 @@
<string name="sender_openpgp_keystore">發件人OpenPGP簽名私鑰可選</string>
<string name="invalid_sender_keystore">發件人簽名私鑰無效</string>
<string name="recipient_email">收件人郵箱</string>
<string name="keystore_path">證書路徑</string>
<string name="keystore_path_tips">可選,下載證書文件到 Download 目錄</string>
<string name="keystore_base64">指定證書</string>
<string name="keystore_base64_tips">可選,下載證書文件到 Download 目錄</string>
<string name="keystore_password">證書密碼</string>
<string name="keystore_password_tips">「私鑰證書」相對應的導入密碼</string>
<string name="email_title">郵件主題</string>

@ -262,8 +262,8 @@
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥可选</string>
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
<string name="recipient_email">收件人邮箱</string>
<string name="keystore_path">证书路径</string>
<string name="keystore_path_tips">可选,下载证书文件到 Download 目录</string>
<string name="keystore_base64">指定证书</string>
<string name="keystore_base64_tips">可选,下载证书文件到 Download 目录</string>
<string name="keystore_password">证书密码</string>
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
<string name="email_title">邮件主题</string>

Loading…
Cancel
Save