mirror of
https://github.com/pppscn/SmsForwarder
synced 2024-11-09 19:10:51 +00:00
新增:发送通道电子邮箱
支持S/MIME
或OpenPGP
加密 #417
优化:以`Base64`形式保存证书(同时兼容`文件路径`形式) #437
This commit is contained in:
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…
Reference in New Issue
Block a user