CMDB 数据加密 最终整合API验证+AES数据加密
当CMDB运行在内网的时候,经过API验证的三关是没有问题的,但是如果运行在外网,有一个问题是,黑客截取后的访问速度比客户端快的时候还会造成数据泄露。为了解决这个问题,就要对数据进行加密
RSA加密
RSA是一种非对称加密,但是对加密的字符串的长度需要设置。但是字符串的长度是不固定的,而且这种方式的加密,加密的字符串越长,加密的时间越长。
- pip install rsa
- rsa.newkeys(256) 256代表加密的位数 256/8=32 就是32个字节,但是源码中32-11=21 也就是最多加密21个字节
import rsa
import base64
# ######### 1. 生成公钥私钥 #########
pub_key_obj, priv_key_obj = rsa.newkeys(256)
pub_key_str = pub_key_obj.save_pkcs1()
pub_key_code = base64.standard_b64encode(pub_key_str)
priv_key_str = priv_key_obj.save_pkcs1()
priv_key_code = base64.standard_b64encode(priv_key_str)
print(pub_key_code)
print(priv_key_code)
# ######### 2. 加密 #########
def encrypt(value):
key_str = base64.standard_b64decode(pub_key_code)
pk = rsa.PublicKey.load_pkcs1(key_str)
val = rsa.encrypt(value.encode('utf-8'), pk)
return val
# ######### 3. 解密 #########
def decrypt(value):
key_str = base64.standard_b64decode(priv_key_code)
pk = rsa.PrivateKey.load_pkcs1(key_str)
val = rsa.decrypt(value, pk)
return val
# ######### 基本使用 #########
if __name__ == '__main__':
v = 'Django'
v1 = encrypt(v)
print(v1)
v2 = decrypt(v1)
print(v2.decode('utf8'))
AES 加密
微信公众号的加密方式,而且对字符串的长度没有限制
但是要加密的字符串必须是16个字节或者是16个字节的倍数
安装模块
由于用到了Crypto,通过pip安装后Python3.6中仍然不能运行。自己是用的Anconda中的自带的,可以使用。
在GitHub找到了已经编译好的Python3.5的pycrypto.
加密过程
- 加密用到的key 必须是16个字节或16字节的倍数
- 被加密的字符串同样也必须是16个字节或16字节的倍数
- 通过bytearray能对字节的长度进行修改
- 用空格补足的算法 对bytearray用16取余,16-余数+原来的长度就补到了32
# ############## 加密 ##############
from Crypto.Cipher import AES
key = b'jlaksdflj77asdfh' # 16个字节或16字节的倍数
cipher = AES.new(key, AES.MODE_CBC, key)
data = '要加密的字符串' # 一个中文是3个字节
byte_data = bytearray(data, encoding='utf8') # 想要动态修改字节,用bytearray 相当于把字节转换成一个数组
# print(len(byte_data))
v1 = len(byte_data) # 这是要加密的数据的长度 21
v2 = v1 % 16 # 取余 5
v3 = 16 - v2 # 这是要补足的数 11 : 21+11=32 是16的倍数
for i in range(v3):
byte_data.append(32) # 32代表ASCII中的空格 chr(32) 可以查看
# print(len(byte_data))
final_data = byte_data.decode('utf8') # 把字节解码成字符串
print(final_data) # 加密的内容并加上了补足的空格
msg = cipher.encrypt(final_data) # 进行加密 final_data必须是16个字节或16字节的倍数
print(msg) # 加密后是字节
解密过程
- 下面是直接利用了加密后的字节msg
- 解密后删除空格
# ############## 解密 ##############
msg = b'G\xf47P\xf4X\xbf\x88\xba^;\xbcA\xde(\xb0)\xafA\xb8\xd8\t\xca\xef\xd7\xcdZhw\x98?N'
key = b'jlaksdflj77asdfh' # 需要同样的key
cipher_jm = AES.new(key, AES.MODE_CBC, key)
result = cipher_jm.decrypt(msg) # 对字节进行解密
print(result.decode('utf8').strip()) # 解密后删除空格
不用空格进行补足--机智的加密解密
如果原来要加密的字符串中存在空格,机密后会删除空格,这是一个问题
机智的加密方式
- 当需要补几个字节的时候,就补上几个。如需要11个字节,就补11个11(ascii)
for i in range(v3):
byte_data.append(v3) # 当需要补几个字节的时候,就补上几个。如需要11个字节,就补11个11(ascii)
机智的解密取值
- result[-1] 是取到了最后一个值,而我们加密的时候,补足的值和个数是相同的
- result[0:result[-1]] 就代表真正的数据 牛逼!!!
result = cipher_jm.decrypt(msg)
result_data = result[0:-result[-1]]
例子:
result = b'\xe8\xa6\x81\xe5\x8a\xa0\xe5\xaf\x86\xe7\x9a\x84\xe5\xad\x97\xe7\xac\xa6\xe4\xb8\xb2\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b' # result 是解密后的数据
print('最后一个字节',result[-1]) # 取最后一个值
print('真实的数据',result[0:-result[-1]]) # 获取真实的数据 result[0:-11]
还有一个问题是 v2取余是0的情况
v2取余是0的情况,加密的字符串正好就是16的倍数,那么解密的时候就出问题了,解决问题还得从加密的地方入手。思路是主动设置一个值,如16
v1 = len(byte_data)
v2 = v1 % 16 # 取余 5
if v2 == 0:
v3 = 16 # 字符串正好是16的倍数 余数是0 主动设置成16
else:
v3 = 16 - v2 # 这是要补足的数 11 : 21+11=32 是16的倍数
for i in range(v3):
byte_data.append(v3)
最终封装成函数
from Crypto.Cipher import AES
def encrypt(message):
key = b'jlaksdflj77asdfh' # 16个字节或16字节的倍数
cipher = AES.new(key, AES.MODE_CBC, key)
byte_data = bytearray(message, encoding='utf8') # 想要动态修改字节,用bytearray 相当于把字节转换成一个数组
v1 = len(byte_data) # 这是要加密的数据的长度 21
v2 = v1 % 16 # 取余 5
if v2 == 0:
v3 = 16
else:
v3 = 16 - v2 # 这是要补足的数 11 : 21+11=32 是16的倍数
for i in range(v3):
byte_data.append(v3)
final_data = byte_data.decode('utf8') # 把字节解码成字符串
msg = cipher.encrypt(final_data) # 进行加密 final_data必须是16个字节或16字节的倍数
return msg
# ############## 解密 ##############
def decrypt(msg):
key = b'jlaksdflj77asdfh' # 需要同样的key
cipher = AES.new(key, AES.MODE_CBC, key)
result = cipher.decrypt(msg) # 对字节进行解密
data = result[0:-result[-1]]
return data.decode('utf8')
if __name__ == '__main__':
msg = encrypt("我要加密了")
data = decrypt(msg)
print(data)
API 加密和AES数据加密结合
客户端发送数据的形式
当客户端把采集的信心向API发送的时候,request.post(url,data,json) ,使用json的的时候默认在请求头中添加'content_type': 'application/json'
,会把字典类型的数据json序列化成字符串发送到API。
# 源码中
if not data and json is not None:
# urllib3 requires a bytes-like body. Python 2's json.dumps
# provides this natively, but Python 3 gives a Unicode string.
content_type = 'application/json'
body = complexjson.dumps(json)
if not isinstance(body, bytes):
body = body.encode('utf-8')
request就是相当于socket,在传输的过程中会把字符串转换成字节发送,所以后台接收的数据实际字节数据。在这里可以自己把数据用json序列化然后转换成字节,用data发送。
import requests
import json
data = {'k1': 'v1'}
data = bytes(json.dumps(data),encoding='utf8')
# response = requests.post("http://127.0.0.1:8000/api/asset.html", headers={'OpenKey': auth()},json={'k1':'v1'})
response = requests.post(
"http://127.0.0.1:8000/api/asset.html",
headers={'OpenKey': auth(), 'content_type': 'application/json'},
data=data
)
print(response.text)
服务端接收数据并解密
elif request.method == 'POST':
server_info= decrypt(request.body) # 对数据进行解密
server_info = json.loads(server_info) # 对字符串反序列化成字典类型的数据
print(server_info)
结合后的客户端
- 把加密和验证制作成插件 lib/utils
from Crypto.Cipher import AES
import time
import hashlib
from config import settings
def encrypt(message):
"""
数据加密函数
:param message:
:return:
"""
key = b'jlaksdflj77asdfh' # 16个字节或16字节的倍数
cipher = AES.new(key, AES.MODE_CBC, key)
byte_data = bytearray(message, encoding='utf8') # 想要动态修改字节,用bytearray 相当于把字节转换成一个数组
v1 = len(byte_data) # 这是要加密的数据的长度 21
v2 = v1 % 16 # 取余 5
if v2 == 0:
v3 = 16
else:
v3 = 16 - v2 # 这是要补足的数 11 : 21+11=32 是16的倍数
for i in range(v3):
byte_data.append(v3)
final_data = byte_data.decode('utf8') # 把字节解码成字符串
msg = cipher.encrypt(final_data) # 进行加密 final_data必须是16个字节或16字节的倍数
return msg
def decrypt(msg):
"""
数据解密函数
:param msg:
:return:
"""
key = b'jlaksdflj77asdfh' # 需要同样的key
cipher = AES.new(key, AES.MODE_CBC, key)
result = cipher.decrypt(msg) # 对字节进行解密
data = result[0:-result[-1]]
return data.decode('utf8')
def auth():
"""
API验证
:return:
"""
ctime = time.time()
key = settings.AUTH_KEY
new_key = '%s|%s' % (key, ctime)
m = hashlib.md5()
m.update(bytes(new_key, encoding='utf8')) # python3中是字节
md5_key = m.hexdigest() # 加密后是字符串
md5_key_time = '%s|%s' % (md5_key, ctime) # 把当前时间发送后API,API利用时间进行加密,最后比较的是密文
return md5_key_time
调用:
- client/Base
- 注意要有请求头,主要是包含OpenKey
class Base(object):
def post_asset(self, server_info):
data = encrypt(json.dumps(server_info)) # 把采集到的资产信息序列化化成字符串 加密后变成字节
# requests.post(settings.API, json=server_info) # 向API发送数据 json=() 内部json序列化
requests.post(settings.API, data=data,headers={'OpenKey': auth(), 'content_type': 'application/json'}) # 向API发送数据 json=() 内部json序列化
结合后的服务端
- server_info= decrypt(request.body) # 对数据进行解密
- server_info = json.loads(server_info) # 对字符串反序列化成字典类型的数据
import json
import hashlib
import time
from django.shortcuts import render, HttpResponse
from repository import models
from api.service import PluginManager
from autoserver import settings
def decrypt(msg):
from Crypto.Cipher import AES
key = b'jlaksdflj77asdfh' # 需要同样的key
cipher = AES.new(key, AES.MODE_CBC, key)
result = cipher.decrypt(msg) # 对字节进行解密
data = result[0:-result[-1]]
return data.decode('utf8')
# 在全局(存在内存中)建立已访问列表,格式是{'key秘钥|时间':超时时间} 后期可以用redis memcache等直接设置超时时间
api_key_record = {
}
def asset(request):
client_md5_citme = request.META.get('HTTP_OPENKEY') # 客户端发送过来的MD5和用于加密的时间
# print(client_md5_citme)
client_md5_key,client_time = client_md5_citme.split('|') # 进行分割
# 第一关:时间验证
client_time = float(client_time) # 把字符串类型的转化成数字
server_time = time.time()
if server_time - client_time > 10:
return HttpResponse("小伙子,超时了,第一关没有通过验证")
# 第二关:规则验证
temp = '%s|%s'%(settings.AUTH_KEY,client_time)
m = hashlib.md5()
m.update(bytes(temp, encoding='utf8'))
server_md5_key = m.hexdigest() # 服务端利用客户端发送的时间进行MD5加密的数据
if server_md5_key!= client_md5_key:
return HttpResponse('第二关没有通过验证')
# 在进入第三关之前删除api_key_record中的超时的值
# 字典不能通过for循环中删除
for k in list(api_key_record.keys()): # 需要把字典的keys()转换成列表
v = api_key_record[k]
if server_time > v:
del api_key_record[k] # 如果超时,则删除api_key_record中相应的记录
# 第三关:建立访问列表(全局)
if client_md5_citme in api_key_record:
return HttpResponse('第三关没有通过验证')
else:
api_key_record[client_md5_citme] = client_time + 10 # 设置超时时间 10s后超时,然后从列表中删除
if request.method == "GET":
aaa = "重要的数据"
return HttpResponse(aaa)
elif request.method == 'POST':
server_info= decrypt(request.body) # 对数据进行解密
server_info = json.loads(server_info) # 对字符串反序列化成字典类型的数据
# print(server_info)
# 新资产信息
# server_info = json.loads(request.body.decode('utf-8')) # 数据存在于body中
print(server_info)
# 数据库中的老的资产信息
hostname = server_info['basic']['data']['hostname'] # 获取主机名
server_obj = models.Server.objects.filter(hostname=hostname).first() # 获得server对象
# print(server_obj) # 这里获得是主机名
if not server_obj:
return HttpResponse('当前主机名在资产中未录入')
asset_obj = server_obj.asset # 直接获得是资产对象 一对一的关系
PluginManager(hostname,server_info,asset_obj,server_obj).exec_plugin()
return HttpResponse('...')