Python 调用 SmsForwarder 主动控制接口案例(使用 SM4 加密)
cnbilinyj opened this issue · comments
中文版
If you can read Chinese or understand Chinese, it is recommended that you read the Chinese version. The author's English is not good, and most of the versions from Chinese to English use machine translation
This is a description specific to the SM4 encryption method. Developers, please ignore this issue and do not mark it as closed or completed. Thank you.
Based on the open-source code content, I found the specific location of the SM4 encryption code: app/src/main/java/com/idormy/sms/forwarder/utils/SM4Crypt.kt
.
Within it, I discovered the default IV value, which is hardcoded on line 22. However, to encode it into a length of 8 bits per byte, the actual value is: 03050609060905090305060906090509
.
I also identified the possible operational modes and padding modes that may be used:
Operational Mode | Padding Mode 1 | Padding Mode 2 | Padding Mode 3 |
---|---|---|---|
CBC | No Padding | PKCS5 | PKCS7 |
ECB | No Padding | PKCS5 | PKCS7 |
These are specifically implemented on lines 15 to 20. Through practical testing, it was determined that the operational mode in use is CBC
, while ECB
is not suitable. The padding modes that were tested and proven effective are PKCS7
and "No Padding" (it is uncertain if other modes are functional).
The above data has been personally tested and proven effective. Testing was conducted on a Vivo x20a
device.
As for the remaining details, such as the secret key and plaintext/ciphertext, you will need to fill them in yourself, as they are specific to your usage scenario.
For Python encryption and decryption methods, testing was conducted using pydroid 3. Please ensure that the gmssl
library has been installed.
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from gmssl.func import bytes_to_list, list_to_bytes
# PKCS7填充
def pkcs7_padding(data, block_size=16):
"""
PKCS#7 populates the given data.
: Param data: the byte string to be padded (bytes)
: Param block_size: block size in bytes, defaults to 16 bytes (128 bits)
: Return: padded byte string
"""
# Calculate the number of bytes to be padded
padding_size = block_size - (len(data) % block_size)
# Creates a string of padding bytes, with the value of each byte being the padding size.
padding = bytes([padding_size]) * padding_size
# Appends the padding byte string to the original data.
return data + padding
def remove_pkcs7_padding(data):
# Remove PKCS#7 padding
padding = ord(bytes([data[-1]]).decode("utf-8"))
if padding > len(data):
# raise ValueError("Invalid padding")
return data
return data[:-padding]
def zeros_padding(data, block_size=16):
"""
Zeros populates the given data.
: Param data: the byte string to be padded (bytes)
: Param block_size: block size in bytes, defaults to 16 bytes (128 bits)
: Return: padded byte string
"""
# Calculate the number of bytes to be padded
padding_size = block_size - (len(data) % block_size)
# Creates a string of padding bytes, with the value of each byte being the padding size.
padding = b'\0' * padding_size
# Appends the padding byte string to the original data.
return data + padding
def remove_zeros_padding(data):
return data.rstrip(b'\0')
# Set the key and IV value.
key = bytes.fromhex('0123456789ABCDEF0123456789ABCDEF') # Placeholder, please replace it with the actual encryption and decryption key.
iv = bytes.fromhex('03050609060905090305060906090509')
def sm4_encrypt(data):
# Fill in data, because SM4 encryption requires multiples of 16 bytes.
pad_data = pkcs7_padding(data)
# Create an SM4 encrypted object
cipher = CryptSM4()
cipher.set_key(key, SM4_ENCRYPT)
# Encrypt data (CBC mode requires manual processing of IV)
encrypted_data = cipher.crypt_cbc(iv, pad_data)
return encrypted_data.hex()[:len(pad_data) * 2]
def sm4_decode(encrypted_data):
cipher = CryptSM4()
cipher.set_key(key, SM4_DECRYPT)
# Decrypt data (CBC mode requires manual processing of IV)
decrypted_data = cipher.crypt_cbc(iv, encrypted_data)
# Delete padding
decrypted_data = remove_pkcs7_padding(decrypted_data)
return decrypted_data
wiki里面也有一个Python案例了:Python 调用 SmsForwarder 主动控制接口案例(使用 SM4 加密)
@pppscn
我知道,但是,虽然我和你都有VPN,但是总会有人没有啊,那里面除了我的这一条以外,另外一个人的是存放在GitHub Gists里面的,而GitHub Gists不连接VPN是打不开的,而我在这里其实只是发了个双语版(后来改成纯英语版了,加了个链接指向中文版的),而且即使不连接VPN也 可能 可以打开
(说了这么多,实际上就是我想在有些人想要问怎么进行SM4加解密的时候看到这条Issue,能解决一小半的问题,毕竟我一开始就是进行解密时发现怎么解密都不成功,始终有一些字符有问题,最后才发现是加密时设置了IV,解密时没有)
SmsForwarder使用 SM4 加密主动控制接口Python调用案例
作者:送命或循环
import base64
import hmac
import time
import json
import urllib.parse
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
class SmsF:
"""
SmsF class
文档: https://github.com/pppscn/SmsForwarder/wiki/%E9%99%84%E5%BD%952%EF%BC%9A%E4%B8%BB%E5%8A%A8%E8%AF%B7%E6%B1%82(%E8%BF%9C%E7%A8%8B%E6%8E%A7%E5%88%B6)
"""
def __init__(self, host: str, key_str: str):
"""
__init__ 初始化
:param key_str: APP 生成的秘钥字符串
:param host: 接口域名
"""
self.host = host
self.key_str = key_str
self.key = bytes.fromhex(key_str)
self.iv = bytes([3, 5, 6, 9, 6, 9, 5, 9, 3, 5, 6, 9, 6, 9, 5, 9])
self.sm4 = CryptSM4()
def _encode(self, _data: dict):
"""
encode 加密参数
:param _data: 待加密数据
:return: 加密后的16 进制字符串
"""
_data = json.dumps(_data)
self.sm4.set_key(self.key, SM4_ENCRYPT)
_sec = self.sm4.crypt_cbc(self.iv, bytes(_data, encoding="utf8"))
return _sec.hex()
def _decode(self, _data):
"""
decode 解码参数
:param _data: 待解码字符串
:return: 解码后的字符串
"""
self.sm4.set_key(self.key, SM4_DECRYPT)
decrypt_value = self.sm4.crypt_cbc(self.iv, bytes.fromhex(_data)) # bytes类型
# print("国密解码后数据:", decrypt_value)
return decrypt_value.decode("utf8")
def _get_sign(self):
"""
get_sign 获取签名
:return: 签名和时间戳
"""
# 把 timestamp+"\n"+密钥 当做签名字符串,使用 HmacSHA256 算法计算签名,然后进行 Base64 encode,最后再把签名参数再进行urlEncode,得到最终的签名(需要使用UTF-8字符集)
timestamp = int(round(time.time() * 1000))
data = str(timestamp) + "\n" + self.key_str
data_bytes = data.encode('utf-8')
# 计算HmacSHA256
signature = hmac.new(self.key, data_bytes, digestmod='sha256').digest()
# Base64 编码
base64_signature = base64.b64encode(signature).decode('utf-8')
# urlEncode
url_encoded_signature = urllib.parse.quote(base64_signature, safe='')
return url_encoded_signature, timestamp
def _get(self, _api: str, _data: dict = None) -> dict:
"""
_get 发送请求
:param _api: 接口地址
:param _data: 请求参数
:return: 请求结果
"""
# 处理默认值
if _data is None:
_data = {}
# 获取签名和时间戳
_sign, _timestamp = self._get_sign()
# 组装参数
_param = {
"data": _data,
"timestamp": _timestamp,
"sign": _sign
}
_param_enc = self._encode(_param)
# 请求数据,参数为加密后的参数
import requests
_res = requests.post(_api, headers={'content-type': "application/json"}, data=_param_enc)
# 解码返回结果
_res_dec = self._decode(_res.text)
# 转换返回数据为字典
_resp = json.loads(_res_dec)
if _resp["code"] != 200:
raise Exception(_resp["msg"])
# 返回结果
return _resp
def query_config(self):
"""
query_config 查询配置
:return: 配置信息
"""
_api = self.host + "/config/query"
return self._get(_api)
def clone_pull(self, _version: int):
"""
clone_pull 一键换新机
:param _version: 客户端App版本号(服务端与客户端的版本号必须一致)
:return: 配置信息
"""
if _version is None:
raise Exception("version is None")
_api = self.host + "/clone/pull"
_param = {
"version_code": _version
}
return self._get(_api, _param)
def clone_push(self, _data: dict):
"""
clone_push 一键换新机
:param _data: 请求参数(参见https://github.com/pppscn/SmsForwarder/wiki/%E9%99%84%E5%BD%952%EF%BC%9A%E4%B8%BB%E5%8A%A8%E8%AF%B7%E6%B1%82(%E8%BF%9C%E7%A8%8B%E6%8E%A7%E5%88%B6)#212-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%91%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8E%A8%E9%80%81%E9%85%8D%E7%BD%AE)
:return: 是否成功等信息
"""
_api = self.host + "/clone/push"
return self._get(_api, _data)
def sms_send(self, _slot: int, _phone: str, _content: str):
"""
sms_send 发送短信
:param _slot: 卡槽(1:卡1,2:卡2)
:param _phone: 手机号(多个手机号用英文分号[;]分隔)
:param _content: 短信内容
:return: 是否成功等信息
"""
_api = self.host + "/sms/send"
_param = {
"sim_slot": _slot,
"phone_numbers": _phone,
"msg_content": _content
}
return self._get(_api, _param)
def sms_query(self, _type: int = 1, _page: int = 1, _size: int = 10, _keyword: str = None):
"""
sms_query 查询短信
:param _type: 1:收件箱,2:发件箱
:param _page: 页码
:param _size: 每页数量
:param _keyword: 查询关键字
:return: 短信列表
"""
_api = self.host + "/sms/query"
_param = {
"type": _type,
"page_num": _page,
"page_size": _size
}
if _keyword is not None:
_param["keyword"] = _keyword
return self._get(_api, _param)
def call_query(self, _type: int = 0, _page: int = 1, _size: int = 10, _phone: str = None):
"""
call_query 查询通话记录
:param _type: 通话类型:1=呼入, 2=呼出, 3=未接,0=不筛选(默认)
:param _page: 页码
:param _size: 每页数量
:param _phone: 查询手机号(模糊匹配)
:return: 通话记录
"""
_api = self.host + "/call/query"
_param = {
"type": _type,
"page_num": _page,
"page_size": _size
}
if _phone is not None:
_param["phone_number"] = _phone
return self._get(_api, _param)
def contact_query(self, _phone: str = None, _name: str = None):
"""
contact_query 查询联系人
:param _phone: 查询手机号(模糊匹配)
:param _name: 查询姓名(模糊匹配)
:return: 联系人列表
"""
_api = self.host + "/contact/query"
_param = {}
if _phone is not None:
_param["phone_number"] = _phone
if _name is not None:
_param["name"] = _name
return self._get(_api, _param)
def contact_add(self, _name: str, _phone: str):
"""
contact_add 添加联系人
:param _name: 姓名
:param _phone: 手机号,多个用半角分号分隔,例:15888888888;19999999999
:return: 是否成功等信息
"""
_api = self.host + "/contact/add"
_param = {
"name": _name,
"phone_number": _phone
}
return self._get(_api, _param)
def battery_query(self):
"""
battery_query 查询电量
:return: 电量信息
"""
_api = self.host + "/battery/query"
return self._get(_api)
def wol_send(self, _mac:str, _ip:str = None, _port:int = 9):
"""
wol_send 发送WOL唤醒包
:param _mac: 网卡MAC地址
:param _ip: IP地址
:param _port: 端口号:7 或 9,默认:9;仅传入ip节点时有效
:return: 是否成功等信息
"""
_api = self.host + "/wol/send"
_param = {
"mac": _mac
}
if _ip is not None:
_param["ip"] = _ip
if _port is not None:
if _ip is None:
raise Exception("ip is None")
_param["port"] = _port
return self._get(_api, _param)
def location_query(self):
"""
location_query 查询定位
:return: 定位信息
"""
_api = self.host + "/location/query"
return self._get(_api)
@pppscn 我知道,但是,虽然我和你都有VPN,但是总会有人没有啊,那里面除了我的这一条以外,另外一个人的是存放在GitHub Gists里面的,而GitHub Gists不连接VPN是打不开的,而我在这里其实只是发了个双语版(后来改成纯英语版了,加了个链接指向中文版的),而且即使不连接VPN也 可能 可以打开 (说了这么多,实际上就是我想在有些人想要问怎么进行SM4加解密的时候看到这条Issue,能解决一小半的问题,毕竟我一开始就是进行解密时发现怎么解密都不成功,始终有一些字符有问题,最后才发现是加密时设置了IV,解密时没有)
所以放在wiki比较好,国内镜像仓库同步了