新增:支持Bark推送加密 #273 (详见:https://bark.day.app/#/encryption)

This commit is contained in:
pppscn 2023-05-13 23:32:04 +08:00
parent f5de522967
commit 1bc2668ab2
7 changed files with 216 additions and 77 deletions

View File

@ -19,4 +19,10 @@ data class BarkSetting(
val level: String? = "active",
//标题模板
val title: String? = "",
//加密算法
val transformation: String = "none",
//加密密钥
val key: String = "",
//初始偏移向量
val iv: String = "",
) : Serializable

View File

@ -46,6 +46,7 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
private val viewModel by viewModels<SenderViewModel> { BaseViewModelFactory(context) }
private var mCountDownHelper: CountDownButtonHelper? = null
private var barkLevel: String = "active" //通知级别
private var transformation: String = "none" //加密算法
@JvmField
@AutoWired(name = KEY_SENDER_ID)
@ -103,6 +104,18 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
}
binding!!.spLevel.selectedIndex = 0
binding!!.spEncryptionAlgorithm.setItems(BARK_ENCRYPTION_ALGORITHM_MAP.values.toList())
binding!!.spEncryptionAlgorithm.setOnItemSelectedListener { _: MaterialSpinner?, _: Int, _: Long, item: Any ->
BARK_ENCRYPTION_ALGORITHM_MAP.forEach {
if (it.value == item) transformation = it.key
}
}
binding!!.spEncryptionAlgorithm.setOnNothingSelectedListener {
binding!!.spEncryptionAlgorithm.selectedIndex = 0
transformation = "none"
}
binding!!.spEncryptionAlgorithm.selectedIndex = 0
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -112,43 +125,43 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
//编辑
binding!!.btnDel.setText(R.string.del)
AppDatabase.getInstance(requireContext())
.senderDao()
.get(senderId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Sender> {
override fun onSubscribe(d: Disposable) {}
AppDatabase.getInstance(requireContext()).senderDao().get(senderId).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : SingleObserver<Sender> {
override fun onSubscribe(d: Disposable) {}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onSuccess(sender: Sender) {
if (isClone) {
titleBar?.setSubTitle(getString(R.string.clone_sender) + ": " + sender.name)
binding!!.btnDel.setText(R.string.discard)
} else {
titleBar?.setSubTitle(getString(R.string.edit_sender) + ": " + sender.name)
}
binding!!.etName.setText(sender.name)
binding!!.sbEnable.isChecked = sender.status == 1
val settingVo = Gson().fromJson(sender.jsonSetting, BarkSetting::class.java)
Log.d(TAG, settingVo.toString())
if (settingVo != null) {
binding!!.etServer.setText(settingVo.server)
binding!!.etGroup.setText(settingVo.group)
binding!!.etIcon.setText(settingVo.icon)
binding!!.etSound.setText(settingVo.sound)
binding!!.etBadge.setText(settingVo.badge)
binding!!.etUrl.setText(settingVo.url)
BARK_LEVEL_MAP.forEach {
if (it.key == settingVo.level) binding!!.spLevel.setSelectedItem(it.value)
}
binding!!.etTitleTemplate.setText(settingVo.title)
}
override fun onSuccess(sender: Sender) {
if (isClone) {
titleBar?.setSubTitle(getString(R.string.clone_sender) + ": " + sender.name)
binding!!.btnDel.setText(R.string.discard)
} else {
titleBar?.setSubTitle(getString(R.string.edit_sender) + ": " + sender.name)
}
})
binding!!.etName.setText(sender.name)
binding!!.sbEnable.isChecked = sender.status == 1
val settingVo = Gson().fromJson(sender.jsonSetting, BarkSetting::class.java)
Log.d(TAG, settingVo.toString())
if (settingVo != null) {
binding!!.etServer.setText(settingVo.server)
binding!!.etGroup.setText(settingVo.group)
binding!!.etIcon.setText(settingVo.icon)
binding!!.etSound.setText(settingVo.sound)
binding!!.etBadge.setText(settingVo.badge)
binding!!.etUrl.setText(settingVo.url)
BARK_LEVEL_MAP.forEach {
if (it.key == settingVo.level) binding!!.spLevel.setSelectedItem(it.value)
}
binding!!.etTitleTemplate.setText(settingVo.title)
BARK_ENCRYPTION_ALGORITHM_MAP.forEach {
if (it.value == settingVo.transformation) binding!!.spEncryptionAlgorithm.setSelectedItem(it.value)
}
binding!!.etEncryptionKey.setText(settingVo.key)
binding!!.etEncryptionIv.setText(settingVo.iv)
}
}
})
}
@ -172,18 +185,22 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
Thread {
@ -202,25 +219,21 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
}.start()
return
}
R.id.btn_del -> {
if (senderId <= 0 || isClone) {
popToBack()
return
}
MaterialDialog.Builder(requireContext())
.title(R.string.delete_sender_title)
.content(R.string.delete_sender_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(senderId)
XToastUtils.success(R.string.delete_sender_toast)
popToBack()
}
.show()
MaterialDialog.Builder(requireContext()).title(R.string.delete_sender_title).content(R.string.delete_sender_tips).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(senderId)
XToastUtils.success(R.string.delete_sender_toast)
popToBack()
}.show()
return
}
R.id.btn_save -> {
val name = binding!!.etName.text.toString().trim()
if (TextUtils.isEmpty(name)) {
@ -262,8 +275,17 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
throw Exception(getString(R.string.invalid_bark_url))
}
val title = binding!!.etTitleTemplate.text.toString().trim()
val key = binding!!.etEncryptionKey.text.toString().trim()
val iv = binding!!.etEncryptionIv.text.toString().trim()
if (transformation.startsWith("AES128") && (key.length != 16 || iv.length != 16)) {
throw Exception(getString(R.string.bark_encryption_key_error1))
} else if (transformation.startsWith("AES192") && (key.length != 24 || iv.length != 24)) {
throw Exception(getString(R.string.bark_encryption_key_error2))
} else if (transformation.startsWith("AES256") && (key.length != 32 || iv.length != 32)) {
throw Exception(getString(R.string.bark_encryption_key_error3))
}
return BarkSetting(server, group, icon, sound, badge, url, barkLevel, title)
return BarkSetting(server, group, icon, sound, badge, url, barkLevel, title, transformation, key, iv)
}
override fun onDestroyView() {

View File

@ -167,6 +167,15 @@ val BARK_LEVEL_MAP = mapOf(
"timeSensitive" to getString(R.string.bark_level_timeSensitive),
"passive" to getString(R.string.bark_level_passive)
)
val BARK_ENCRYPTION_ALGORITHM_MAP = mapOf(
"none" to getString(R.string.bark_encryption_algorithm_none),
"AES128/CBC/PKCS7Padding" to "AES128/CBC/PKCS7Padding",
"AES128/ECB/PKCS7Padding" to "AES128/ECB/PKCS7Padding",
"AES192/CBC/PKCS7Padding" to "AES192/CBC/PKCS7Padding",
"AES192/ECB/PKCS7Padding" to "AES192/ECB/PKCS7Padding",
"AES256/CBC/PKCS7Padding" to "AES256/CBC/PKCS7Padding",
"AES256/ECB/PKCS7Padding" to "AES256/ECB/PKCS7Padding",
)
//发送通道
const val TYPE_DINGTALK_GROUP_ROBOT = 0

View File

@ -1,6 +1,7 @@
package com.idormy.sms.forwarder.utils.sender
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import com.google.gson.Gson
import com.idormy.sms.forwarder.database.entity.Rule
@ -13,8 +14,13 @@ import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.cache.model.CacheMode
import com.xuexiang.xhttp2.callback.SimpleCallBack
import com.xuexiang.xhttp2.exception.ApiException
import java.net.URLEncoder
import java.util.regex.Pattern
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@Suppress("unused")
class BarkUtils {
companion object {
@ -52,29 +58,40 @@ class BarkUtils {
XHttp.post(requestUrl)
}
request.params("title", title)
.params("body", content)
.params("isArchive", 1)
if (!TextUtils.isEmpty(setting.group)) request.params("group", setting.group)
if (!TextUtils.isEmpty(setting.icon)) request.params("icon", setting.icon)
if (!TextUtils.isEmpty(setting.level)) request.params("level", setting.level)
if (!TextUtils.isEmpty(setting.sound)) request.params("sound", setting.sound)
if (!TextUtils.isEmpty(setting.badge)) request.params("badge", setting.badge)
if (!TextUtils.isEmpty(setting.url)) request.params("url", setting.url)
val msgMap: MutableMap<String, Any> = mutableMapOf()
msgMap["title"] = title
msgMap["body"] = content
msgMap["isArchive"] = 1
if (!TextUtils.isEmpty(setting.group)) msgMap["group"] = setting.group.toString()
if (!TextUtils.isEmpty(setting.icon)) msgMap["icon"] = setting.icon.toString()
if (!TextUtils.isEmpty(setting.level)) msgMap["level"] = setting.level.toString()
if (!TextUtils.isEmpty(setting.sound)) msgMap["sound"] = setting.sound.toString()
if (!TextUtils.isEmpty(setting.badge)) msgMap["badge"] = setting.badge.toString()
if (!TextUtils.isEmpty(setting.url)) msgMap["url"] = setting.url.toString()
val isCode: Int = content.indexOf("验证码")
val isPassword: Int = content.indexOf("动态密码")
val isPassword2: Int = content.indexOf("短信密码")
if (isCode != -1 || isPassword != -1 || isPassword2 != -1) {
val p = Pattern.compile("(\\d{4,6})")
val m = p.matcher(content)
if (m.find()) {
println(m.group())
request.params("automaticallyCopy", "1")
request.params("copy", m.group())
//自动复制验证码
val pattern = Regex("(?<!回复)(验证码|授权码|校验码|检验码|确认码|激活码|动态码|安全码|(验证)?代码|校验代码|检验代码|激活代码|确认代码|动态代码|安全代码|登入码|认证码|识别码|短信口令|动态密码|交易码|上网密码|动态口令|随机码|驗證碼|授權碼|校驗碼|檢驗碼|確認碼|激活碼|動態碼|(驗證)?代碼|校驗代碼|檢驗代碼|確認代碼|激活代碼|動態代碼|登入碼|認證碼|識別碼|一次性密码|[Cc][Oo][Dd][Ee]|[Vv]erification)")
if (pattern.containsMatchIn(content)) {
var code = content.replace("(.*)((代|授权|验证|动态|校验)码|[【\\[].*[】\\]]|[Cc][Oo][Dd][Ee]|[Vv]erification\\s?([Cc]ode)?)\\s?(G-|<#>)?([:\\s是为]|[Ii][Ss]){0,3}[\\(\\[【{「]?(([0-9\\s]{4,7})|([\\dA-Za-z]{5,6})(?!([Vv]erification)?([Cc][Oo][Dd][Ee])|:))[」}】\\]\\)]?(?=([^0-9a-zA-Z]|\$))(.*)".toRegex(), "$7").trim()
code = code.replace("[^\\d]*[\\(\\[【{「]?([0-9]{3}\\s?[0-9]{1,3})[」}】\\]\\)]?(?=.*((代|授权|验证|动态|校验)码|[【\\[].*[】\\]]|[Cc][Oo][Dd][Ee]|[Vv]erification\\s?([Cc]ode)?))(.*)".toRegex(), "$1").trim()
if (code.isNotEmpty()) {
msgMap["copy"] = code
msgMap["automaticallyCopy"] = 1
}
}
val requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg")
//推送加密
if (setting.transformation.isNotEmpty() && "none" != setting.transformation && setting.key.isNotEmpty() && setting.iv.isNotEmpty()) {
var transformation = setting.transformation.replace("AES128", "AES").replace("AES192", "AES").replace("AES256", "AES")
val ciphertext = encrypt(requestMsg, transformation, setting.key, setting.iv)
request.params("ciphertext", URLEncoder.encode(ciphertext, "UTF-8"))
request.params("iv", URLEncoder.encode(setting.iv, "UTF-8"))
} else {
request.upJson(requestMsg)
}
request.ignoreHttpsCert() //忽略https证书
.keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
@ -106,5 +123,14 @@ class BarkUtils {
}
fun encrypt(plainText: String, transformation: String, key: String, iv: String): String {
val cipher = Cipher.getInstance(transformation)
val keySpec = SecretKeySpec(key.toByteArray(), "AES")
val ivSpec = IvParameterSpec(iv.toByteArray())
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val encryptedBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
return Base64.encode(encryptedBytes, Base64.NO_WRAP).toString()
}
}
}

View File

@ -303,6 +303,74 @@
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:text="@string/bark_encryption_algorithm"
android:textStyle="bold" />
<com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner
android:id="@+id/sp_encryption_algorithm"
style="@style/Material.SpinnerStyle"
android:layout_marginStart="5dp" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bark_encryption_key"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_encryption_key"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/bark_encryption_key_tips"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bark_encryption_iv"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_encryption_iv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/bark_encryption_iv_tips"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -728,12 +728,16 @@
<string name="bark_url_regex" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]]]></string>
<string name="bark_url_regex2" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^[a-z]+://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]]]></string>
<string name="bark_url_error">Url format error</string>
<string name="bark_key">AES Key</string>
<string name="bark_key_tips">Fill in 16 chars to enable push encryption</string>
<string name="bark_iv">AES iv</string>
<string name="bark_iv_tips">Fill in 16 chars to enable push encryption</string>
<string name="bark_key_regex" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^[a-zA-Z0-9]{16}]]></string>
<string name="bark_key_error">AES Key and iv must be 16 characters</string>
<string name="bark_encryption_algorithm">Encryption Algorithm</string>
<string name="bark_encryption_algorithm_none">NONE</string>
<string name="bark_encryption_key">AES Key</string>
<string name="bark_encryption_key_tips">corresponding key on bark</string>
<string name="bark_encryption_iv">AES iv</string>
<string name="bark_encryption_iv_tips">corresponding iv on bark</string>
<string name="bark_encryption_key_regex" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^[a-zA-Z0-9]{16}]]></string>
<string name="bark_encryption_key_error1">AES Key and iv must be 16 characters</string>
<string name="bark_encryption_key_error2">AES Key and iv must be 24 characters</string>
<string name="bark_encryption_key_error3">AES Key and iv must be 32 characters</string>
<string name="from_email_hint">Fill in the username before @</string>
<string name="from_email_full_hint">Fill in the format: AAA@BBB.CCC</string>

View File

@ -729,12 +729,16 @@
<string name="bark_url_regex" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]]]></string>
<string name="bark_url_regex2" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^[a-z]+://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]]]></string>
<string name="bark_url_error">Url格式错误</string>
<string name="bark_key">加密密钥</string>
<string name="bark_key_tips">填写16个字符以启用推送加密</string>
<string name="bark_iv">偏移向量</string>
<string name="bark_iv_tips">填写16个字符以启用推送加密</string>
<string name="bark_key_regex" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^([a-zA-Z0-9]{16})? $]]></string>
<string name="bark_key_error">加密密钥和偏移向量必须同时是16个字符</string>
<string name="bark_encryption_algorithm">加密算法</string>
<string name="bark_encryption_algorithm_none">不加密</string>
<string name="bark_encryption_key">加密密钥</string>
<string name="bark_encryption_key_tips">对应bark上的key</string>
<string name="bark_encryption_iv">偏移向量</string>
<string name="bark_encryption_iv_tips">对应bark上的iv</string>
<string name="bark_encryption_key_regex" formatted="false" tools:ignore="TypographyDashes"><![CDATA[^([a-zA-Z0-9]{16})? $]]></string>
<string name="bark_encryption_key_error1">加密密钥和偏移向量都必须是16位</string>
<string name="bark_encryption_key_error2">加密密钥和偏移向量都必须是24位</string>
<string name="bark_encryption_key_error3">加密密钥和偏移向量都必须是32位</string>
<string name="from_email_hint">填写 @ 前面的用户名</string>
<string name="from_email_full_hint">填写格式: AAA@BBB.CCC</string>