新增:发送通道电子邮箱支持S/MIMEOpenPGP加密 #417

优化:以`Base64`形式保存证书(同时兼容`文件路径`形式) #437
This commit is contained in:
pppscn 2024-04-07 10:50:22 +08:00
parent 40ee077ea7
commit a53fa6db12
8 changed files with 89 additions and 30 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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