safeheron-api-sdk-python 1.0.3__tar.gz → 1.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/PKG-INFO +1 -1
- safeheron_api_sdk_python-1.0.5/safeheron_api_sdk_python/client.py +28 -0
- safeheron_api_sdk_python-1.0.5/safeheron_api_sdk_python/tools.py +256 -0
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/safeheron_api_sdk_python.egg-info/PKG-INFO +2 -2
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/safeheron_api_sdk_python.egg-info/SOURCES.txt +2 -0
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/setup.py +2 -2
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/LICENSE +0 -0
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/README.md +0 -0
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/README.rst +0 -0
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/safeheron_api_sdk_python.egg-info/dependency_links.txt +0 -0
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/safeheron_api_sdk_python.egg-info/top_level.txt +0 -0
- {safeheron_api_sdk_python-1.0.3 → safeheron_api_sdk_python-1.0.5}/setup.cfg +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from safeheron_api_sdk_python.tools import *
|
|
2
|
+
|
|
3
|
+
class Client:
|
|
4
|
+
|
|
5
|
+
def __init__(self, config):
|
|
6
|
+
global api_key
|
|
7
|
+
global platform_pub_key
|
|
8
|
+
global use_private_key
|
|
9
|
+
global base_url
|
|
10
|
+
api_key = config['apiKey']
|
|
11
|
+
platform_pub_key = config['safeheronPublicKey']
|
|
12
|
+
base_url = config['baseUrl']
|
|
13
|
+
if 'privateKey' in config:
|
|
14
|
+
use_private_key = PEM_PRIVATE_HEAD + config['privateKey'] + PEM_PRIVATE_END
|
|
15
|
+
if 'privateKeyPemFile' in config:
|
|
16
|
+
private_key_pem_file = config['privateKeyPemFile']
|
|
17
|
+
if private_key_pem_file is not None and private_key_pem_file != '':
|
|
18
|
+
use_private_key = load_rsa_private_key(private_key_pem_file)
|
|
19
|
+
|
|
20
|
+
def send_request(self, request, uri):
|
|
21
|
+
req = encrypt_request(api_key, request, platform_pub_key, use_private_key)
|
|
22
|
+
res = self.execution(req, uri)
|
|
23
|
+
res.raise_for_status()
|
|
24
|
+
res = res.json()
|
|
25
|
+
return decrypt_response(res, platform_pub_key, use_private_key)
|
|
26
|
+
|
|
27
|
+
def execution(self, request, uri):
|
|
28
|
+
return requests.post(base_url + uri, data=json.dumps(request), headers={"Content-Type": "application/json"})
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Crypto 可直接替换为 Cryptodome
|
|
2
|
+
from Cryptodome.Cipher import AES
|
|
3
|
+
from Cryptodome.Util.Padding import pad, unpad
|
|
4
|
+
from Cryptodome.PublicKey import RSA
|
|
5
|
+
from Cryptodome.Random import get_random_bytes
|
|
6
|
+
from base64 import b64encode, b64decode
|
|
7
|
+
from Cryptodome.Signature import PKCS1_v1_5 as Signature_pkcs1_v1_5
|
|
8
|
+
from Cryptodome.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
|
|
9
|
+
from Cryptodome.Hash import SHA256, SHA1
|
|
10
|
+
from Cryptodome import Random
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
import requests
|
|
14
|
+
from cryptography.hazmat.primitives import serialization
|
|
15
|
+
from cryptography.hazmat.backends import default_backend
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
PEM_PUBLIC_HEAD = "-----BEGIN PUBLIC KEY-----\n"
|
|
19
|
+
PEM_PUBLIC_END = "\n-----END PUBLIC KEY-----"
|
|
20
|
+
PEM_PRIVATE_HEAD = "-----BEGIN RSA PRIVATE KEY-----\n"
|
|
21
|
+
PEM_PRIVATE_END = "\n-----END RSA PRIVATE KEY-----"
|
|
22
|
+
|
|
23
|
+
def load_rsa_private_key(file_path, password=None):
|
|
24
|
+
try:
|
|
25
|
+
with open(file_path, "rb") as key_file:
|
|
26
|
+
private_key = serialization.load_pem_private_key(
|
|
27
|
+
key_file.read(),
|
|
28
|
+
password=password,
|
|
29
|
+
backend=default_backend()
|
|
30
|
+
)
|
|
31
|
+
pem = private_key.private_bytes(
|
|
32
|
+
encoding=serialization.Encoding.PEM,
|
|
33
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
34
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
35
|
+
)
|
|
36
|
+
return pem.decode()
|
|
37
|
+
except Exception as e:
|
|
38
|
+
raise Exception("Failed to parse rsa pem file")
|
|
39
|
+
|
|
40
|
+
def check_key_and_iv(key: bytes, iv: bytes):
|
|
41
|
+
if len(key) != 32:
|
|
42
|
+
raise Exception("aes error: key must have 32 bytes")
|
|
43
|
+
if len(iv) != 16:
|
|
44
|
+
raise Exception("aes error: iv must have 16 bytes")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def aes_encrypt(key: bytes, iv: bytes, message: bytes):
|
|
48
|
+
check_key_and_iv(key, iv)
|
|
49
|
+
message = pad(message, AES.block_size)
|
|
50
|
+
try:
|
|
51
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
52
|
+
encrypt_text = cipher.encrypt(message)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise Exception("aes encrypt error: %s" % e)
|
|
55
|
+
return encrypt_text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def aes_decrypt(key: bytes, iv: bytes, encrypt_text: bytes):
|
|
59
|
+
check_key_and_iv(key, iv)
|
|
60
|
+
try:
|
|
61
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
62
|
+
pad_text = cipher.decrypt(encrypt_text)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise Exception("aes decrypt error: %s" % e)
|
|
65
|
+
return unpad(pad_text, AES.block_size)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_rsa_key(key_pem, passphrase=None):
|
|
69
|
+
if key_pem == '':
|
|
70
|
+
raise Exception("rsa encrypt error: pubkey can't be none")
|
|
71
|
+
if passphrase is None:
|
|
72
|
+
return RSA.import_key(key_pem)
|
|
73
|
+
else:
|
|
74
|
+
return RSA.import_key(key_pem, passphrase=passphrase)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rsa_encrypt(public_key, message: bytes):
|
|
78
|
+
try:
|
|
79
|
+
cipher_rsa = Cipher_pkcs1_v1_5.new(public_key)
|
|
80
|
+
encrypt_text = cipher_rsa.encrypt(message)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise Exception("rsa encrypt error: %s" % e)
|
|
83
|
+
encrypt_text_base64 = b64encode(encrypt_text)
|
|
84
|
+
return encrypt_text_base64.decode('utf-8')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def rsa_decrypt(private_key, message: str):
|
|
88
|
+
encrypt_text = b64decode(message)
|
|
89
|
+
try:
|
|
90
|
+
cipher_rsa = Cipher_pkcs1_v1_5.new(private_key)
|
|
91
|
+
sentinel = Random.new().read(SHA1.digest_size)
|
|
92
|
+
return cipher_rsa.decrypt(encrypt_text, sentinel)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise Exception("rsa decrypt error: %s" % e)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def rsa_sign(private_key, message_hash: bytes):
|
|
98
|
+
try:
|
|
99
|
+
signer = Signature_pkcs1_v1_5.new(private_key)
|
|
100
|
+
digest = SHA256.new()
|
|
101
|
+
digest.update(message_hash)
|
|
102
|
+
signed = signer.sign(digest)
|
|
103
|
+
return b64encode(signed).decode()
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise Exception("rsa sign error: %s" % e)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def rsa_verify(public_key, message_hash: bytes, signature):
|
|
109
|
+
try:
|
|
110
|
+
signer = Signature_pkcs1_v1_5.new(public_key)
|
|
111
|
+
digest = SHA256.new()
|
|
112
|
+
digest.update(message_hash)
|
|
113
|
+
return signer.verify(digest, b64decode(signature))
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise Exception("rsa sign error: %s" % e)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def sort_request(r: dict):
|
|
119
|
+
sortData = json.dumps(r, sort_keys=True).replace(' ', '')
|
|
120
|
+
strArr = []
|
|
121
|
+
data1 = json.loads(sortData)
|
|
122
|
+
for key, value in data1.items():
|
|
123
|
+
# print(value, type(value))
|
|
124
|
+
if isinstance(value, dict): # 注意:对于dict类型的数据,一定要保留双引号,但python默认都是单引号
|
|
125
|
+
val = json.dumps(value).replace(' ', '')
|
|
126
|
+
else:
|
|
127
|
+
val = value
|
|
128
|
+
strArr.append(key + '=' + str(val))
|
|
129
|
+
return "&".join(strArr).encode('utf-8')
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def encrypt_request(api_key, request_dict, platform_rsa_pk, api_user_rsa_sk):
|
|
133
|
+
platform_rsa_pk = get_rsa_key(PEM_PUBLIC_HEAD + platform_rsa_pk + PEM_PUBLIC_END)
|
|
134
|
+
api_user_rsa_sk = get_rsa_key(api_user_rsa_sk)
|
|
135
|
+
|
|
136
|
+
ret = dict()
|
|
137
|
+
|
|
138
|
+
ret['apiKey'] = api_key
|
|
139
|
+
|
|
140
|
+
# prepare aes key and iv
|
|
141
|
+
aes_key = get_random_bytes(32)
|
|
142
|
+
aes_iv = get_random_bytes(16)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# 1 rsa encrypt aes key + iv
|
|
146
|
+
aes_data = aes_key + aes_iv
|
|
147
|
+
ret['key'] = rsa_encrypt(platform_rsa_pk, aes_data)
|
|
148
|
+
|
|
149
|
+
# 2 aes encrypt request data
|
|
150
|
+
if request_dict is not None:
|
|
151
|
+
request_data = json.dumps(request_dict.__dict__).replace('\'', '\"').replace('\n', '').encode('utf-8')
|
|
152
|
+
aes_encrypted_bytes = aes_encrypt(aes_key, aes_iv, request_data)
|
|
153
|
+
ret['bizContent'] = b64encode(aes_encrypted_bytes).decode()
|
|
154
|
+
|
|
155
|
+
# 3 set timestamp
|
|
156
|
+
ret['timestamp'] = str(int(time.time() * 1000))
|
|
157
|
+
|
|
158
|
+
# 4 sign request
|
|
159
|
+
need_sign_message = sort_request(ret)
|
|
160
|
+
ret['sig'] = rsa_sign(api_user_rsa_sk, need_sign_message)
|
|
161
|
+
|
|
162
|
+
return ret
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def decrypt_response(response_dict, platform_rsa_pk, api_user_rsa_sk):
|
|
166
|
+
platform_rsa_pk = get_rsa_key(PEM_PUBLIC_HEAD + platform_rsa_pk + PEM_PUBLIC_END)
|
|
167
|
+
api_user_rsa_sk = get_rsa_key(api_user_rsa_sk)
|
|
168
|
+
required_keys = {
|
|
169
|
+
'key',
|
|
170
|
+
'sig',
|
|
171
|
+
'bizContent',
|
|
172
|
+
'timestamp',
|
|
173
|
+
'code',
|
|
174
|
+
'message'
|
|
175
|
+
}
|
|
176
|
+
missing_keys = required_keys.difference(response_dict.keys())
|
|
177
|
+
if missing_keys:
|
|
178
|
+
raise Exception(response_dict)
|
|
179
|
+
|
|
180
|
+
# 1 rsa verify
|
|
181
|
+
sig = response_dict.pop('sig')
|
|
182
|
+
need_sign_message = sort_request(response_dict)
|
|
183
|
+
v = rsa_verify(platform_rsa_pk, need_sign_message, sig)
|
|
184
|
+
if not v:
|
|
185
|
+
raise Exception("rsa verify: false")
|
|
186
|
+
|
|
187
|
+
# 2 get aes key and iv
|
|
188
|
+
key = response_dict.pop('key')
|
|
189
|
+
aes_data = rsa_decrypt(api_user_rsa_sk, key)
|
|
190
|
+
aes_key = aes_data[0:32]
|
|
191
|
+
aes_iv = aes_data[32:48]
|
|
192
|
+
|
|
193
|
+
# 3 aes decrypt data, get response data
|
|
194
|
+
r = aes_decrypt(aes_key, aes_iv, b64decode(response_dict['bizContent']))
|
|
195
|
+
#response_dict['bizContent'] = json.loads(r.decode())
|
|
196
|
+
|
|
197
|
+
return json.loads(r.decode())
|
|
198
|
+
|
|
199
|
+
#Used for webhook decryption callback request data
|
|
200
|
+
def decrypt_request(response_dict, verify_rsa_pk, decrypt_rsa_sk):
|
|
201
|
+
verify_rsa_pk = get_rsa_key(PEM_PUBLIC_HEAD + verify_rsa_pk + PEM_PUBLIC_END)
|
|
202
|
+
decrypt_rsa_sk = get_rsa_key(decrypt_rsa_sk)
|
|
203
|
+
# 1 rsa verify
|
|
204
|
+
sig = response_dict.pop('sig')
|
|
205
|
+
need_sign_message = sort_request(response_dict)
|
|
206
|
+
v = rsa_verify(verify_rsa_pk, need_sign_message, sig)
|
|
207
|
+
if not v:
|
|
208
|
+
raise Exception("rsa verify: false")
|
|
209
|
+
|
|
210
|
+
# 2 get aes key and iv
|
|
211
|
+
aes_data = rsa_decrypt(decrypt_rsa_sk, response_dict['key'])
|
|
212
|
+
aes_key = aes_data[0:32]
|
|
213
|
+
aes_iv = aes_data[32:48]
|
|
214
|
+
|
|
215
|
+
# 3 aes decrypt data, get response data
|
|
216
|
+
r = aes_decrypt(aes_key, aes_iv, b64decode(response_dict['bizContent']))
|
|
217
|
+
|
|
218
|
+
return json.loads(r.decode())
|
|
219
|
+
|
|
220
|
+
#For webhook to send encrypted response content
|
|
221
|
+
def encrypt_response(raw_data,encrpyt_rsa_pk, sign_rsa_sk):
|
|
222
|
+
encrpyt_rsa_pk = get_rsa_key(PEM_PUBLIC_HEAD + encrpyt_rsa_pk + PEM_PUBLIC_END)
|
|
223
|
+
sign_rsa_sk = get_rsa_key(sign_rsa_sk)
|
|
224
|
+
ret = dict()
|
|
225
|
+
# prepare aes key and iv
|
|
226
|
+
aes_key = get_random_bytes(32)
|
|
227
|
+
aes_iv = get_random_bytes(16)
|
|
228
|
+
res_data = json.dumps(raw_data).replace('\n', '').encode('utf-8')
|
|
229
|
+
|
|
230
|
+
# 1 rsa encrypt aes key + iv
|
|
231
|
+
aes_data = aes_key + aes_iv
|
|
232
|
+
ret['key'] = rsa_encrypt(encrpyt_rsa_pk, aes_data)
|
|
233
|
+
|
|
234
|
+
# 2 aes encrypt request data
|
|
235
|
+
aes_encrypted_bytes = aes_encrypt(aes_key, aes_iv, res_data)
|
|
236
|
+
ret['bizContent'] = b64encode(aes_encrypted_bytes).decode()
|
|
237
|
+
|
|
238
|
+
# 3 set timestamp , no need to add
|
|
239
|
+
ret['timestamp'] = str(int(time.time() * 1000))
|
|
240
|
+
|
|
241
|
+
# 4 code
|
|
242
|
+
ret['code'] = "200"
|
|
243
|
+
|
|
244
|
+
# 5 message
|
|
245
|
+
ret['message'] = "callback_ok"
|
|
246
|
+
|
|
247
|
+
# 6 sign request
|
|
248
|
+
need_sign_message = sort_request(ret)
|
|
249
|
+
ret['sig'] = rsa_sign(sign_rsa_sk, need_sign_message)
|
|
250
|
+
return ret
|
|
251
|
+
|
|
252
|
+
def rsa_gen_key():
|
|
253
|
+
rsa_key = RSA.generate(4096)
|
|
254
|
+
sk = rsa_key.export_key()
|
|
255
|
+
pk = rsa_key.publickey().export_key()
|
|
256
|
+
return sk, pk
|
|
@@ -3,7 +3,7 @@ from setuptools import find_packages
|
|
|
3
3
|
with open("README.rst", "r") as f:
|
|
4
4
|
long_description = f.read()
|
|
5
5
|
setup(name='safeheron_api_sdk_python',
|
|
6
|
-
version='1.0.
|
|
6
|
+
version='1.0.5',
|
|
7
7
|
description='Python for Safeheron API',
|
|
8
8
|
long_description=long_description,
|
|
9
9
|
author='safeheron',
|
|
@@ -11,7 +11,7 @@ setup(name='safeheron_api_sdk_python',
|
|
|
11
11
|
url='https://github.com/Safeheron/safeheron-api-sdk-python',
|
|
12
12
|
install_requires=[],
|
|
13
13
|
license='MIT License',
|
|
14
|
-
packages=find_packages(),
|
|
14
|
+
packages=find_packages(include=['safeheron_api_sdk_python', 'safeheron_api_sdk_python.*']),
|
|
15
15
|
platforms=["all"],
|
|
16
16
|
classifiers=[
|
|
17
17
|
'Operating System :: OS Independent',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|