Client-Side Encryption Practice
Overview
Client-side encryption involves users encrypting and decrypting files locally. Baidu AI Cloud Object Storage does not participate in the encryption or decryption process but only manages file uploading, storing, and downloading. The plaintext key is kept entirely on the user's local device. This method enhances file security, ensuring that even if a file is leaked, others cannot decrypt it to access the original content. This document provides a solution for client-side encryption.
Notes:
- Since users need to be responsible for keeping the key themselves, if the plaintext key is lost, you will not be able to restore the original file content.
Requirement scenarios
Users prefer to handle the encryption and decryption process locally, with the plaintext key stored securely on their own devices.
Solution overview
- Generate an asymmetric key pair (
private_rsa_keyandpublic_rsa_key) locally using the RSA algorithm; - When uploading a file, generate a symmetric key (
aes_key) using the CTR mode of AES 256; - For ordinary files, use the symmetric key
aes_keyto encrypt the original data; use the public keypublic_rsa_keyto encryptaes_keyto generateencrypted_aes_key, which is used as the object’s meta, and then upload the file to Bos. For large files, use Bos’s multipart upload. Each part is encrypted and uploaded. The part size is an integer multiple of 16 bytes. Similarly, use the public keypublic_rsa_keyto encryptaes_keyto generateencrypted_aes_key, which is used as the object’s meta. - When downloading a file, first obtain the file’s meta information to get
encrypted_aes_key, then use the local private keyprivate_rsa_keyto decryptencrypted_aes_keyto getaes_key. For ordinary files, download the encrypted file and use the aes_key to decrypt it to get the original data. For large files, multipart download can be used. The functionget_object(bucket_name, object_key, [range_start, range_end])of the Python SDK is used to download the bytes in the specified range [range_start, range_end] of the large file, including the byte at the ending position range_end, where 0 ≤ range_start ≤ range_end ≤ file size.
Note: When using AES 256 for encryption, the AES algorithm has a block size of 128 bits or 16 bytes. Therefore, when uploading large files in parts, ensure that each part's size is an integer multiple of 16 bytes.
Practice steps
The following is a sample implementation using Python code.
Preparation
- Users need to install the Python SDK, please refer to Install Python SDK.
- Run the following command to install the PyCrypto library.
1pip install pycrypto
- Update the configuration. Replace HOST, AK, and SK in the example code with the appropriate configuration details for this use case.
Example code is as follows
1#coding=utf-8
2
3'''
4The file aims to help client to encrypt data on python sdk with RSA algorithm and symmetric encryption algorithm
5'''
6
7import os
8import shutil
9import base64
10import random
11import string
12
13 #Import configuration files and object storage modules
14from baidubce.bce_client_configuration import BceClientConfiguration
15from baidubce.auth.bce_credentials import BceCredentials
16from baidubce import exception
17from baidubce.services import bos
18from baidubce.services.bos import canned_acl
19from baidubce.services.bos.bos_client import BosClient
20
21 #Import Crypto encryption module
22from Crypto import Random
23from Crypto.Cipher import AES
24from Crypto.Cipher import PKCS1_OAEP
25from Crypto.PublicKey import RSA
26from Crypto.Util import Counter
27
28 #Set the symmetric key length to 128 bits
29_AES256_KEY_SIZE = 32
30 #Set the Counter length for AES CTR mode
31_COUNTER_BITS_LENGTH_AES_CTR = 8*16
32
33class CipherWithAES:
34# start is the initial value of the CTR counter
35 def __init__(self, key= None, start= None):
36 if not key:
37 key = Random.new().read(_AES256_KEY_SIZE)
38 if not start:
39 start = random.randint(1,100)
40 self.key = key
41 self.start = start
42 #Generate counter
43 my_counter = Counter.new(_COUNTER_BITS_LENGTH_AES_CTR, initial_value=self.start)
44 #Generate AES object
45 self.cipher = AES.new(self.key, AES.MODE_CTR, counter = my_counter)
46
47 #Encrypt data
48 def encrypt(self, plaintext):
49 return self.cipher.encrypt(plaintext)
50
51 #Decrypt data
52 def decrypt(self, ciphertext):
53 return self.cipher.decrypt(ciphertext)
54
55class CipherWithRSA:
56# The input parameters are the public key file name and the private key file name
57 def __init__(self, public_key_file_name = None, private_key_file_name = None):
58 self.public_key_file_name=public_key_file_name
59 self.private_key_file_name = private_key_file_name
60 if not self.public_key_file_name:
61 self.public_key_file_name = "rsa_public_key.pem"
62 if not self.private_key_file_name:
63 self.private_key_file_name = "rsa_private_key.pem"
64 #If no public key and private key files are input, generate an RSA key pair locally
65 if not (os.path.isfile(self.public_key_file_name) and os.path.isfile(self.private_key_file_name)):
66 self._generate_rsa_key()
67 return
68 #Read the public key and private key from the files and generate RSA objects
69 with open(self.public_key_file_name) as file_key:
70 public_key_obj_rsa = RSA.importKey(file_key.read())
71 self.encrypt_obj = PKCS1_OAEP.new(public_key_obj_rsa)
72 with open(self.private_key_file_name) as file_key:
73 private_key_obj_rsa = RSA.importKey(file_key.read())
74 self.decrypt_obj = PKCS1_OAEP.new(private_key_obj_rsa)
75
76 #Generate RSA key pair locally
77 def _generate_rsa_key(self):
78 private_key_obj_rsa = RSA.generate(2048)
79 public_key_obj_rsa = private_key_obj_rsa.publickey()
80 self.encrypt_obj = PKCS1_OAEP.new(public_key_obj_rsa)
81 self.decrypt_obj = PKCS1_OAEP.new(private_key_obj_rsa)
82 #Store the generated key pair locally
83 with open(self.public_key_file_name,"w") as file_export:
84 file_export.write(public_key_obj_rsa.exportKey())
85 with open(self.private_key_file_name,"w") as file_export:
86 file_export.write(private_key_obj_rsa.exportKey())
87
88 #Encrypt data
89 def encrypt(self,plaintext_key):
90 return self.encrypt_obj.encrypt(plaintext_key)
91
92 #Decrypt data
93 def decrypt(self,ciphertext_key):
94 return self.decrypt_obj.decrypt(ciphertext_key)
95 #Generate a random file name
96def _random_string(length):
97 return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length))
98
99################put super file#################
100 #Upload large files to Baidu Object Storage in parts. "part_size" is the part size, which must be an integer multiple of 16 bytes
101def put_super_file(bos_client, bucket_name, super_object_key, super_file, part_size,cipher_rsa):
102 """
103 Put super file to baidu object storage by multipart uploading."part_size" must be multiple of 16 bytes
104
105 :param bos_client:bos client
106 :type bos_client:baidubce.services.bos.bos_client.BosClient
107
108 :param bucket_name:None
109 :type bucket_name:string
110
111 :param super_object_key: destion object key of super file
112 :type super_object_key: string
113
114 :param super_file: super file name
115 :type super_file:string
116
117 :param part_size: size of part to upload once,"part_size" must be multiple of 16 bytes and more than 5MB
118 :type: int
119
120 :param cipher_rsa: encrypt symmetric key
121 :type cipher_rsa: CipherWithRSA
122
123 :return :**Http Response**
124
125 """
126 #1.initial
127 try:
128 if not isinstance(bos_client,BosClient):
129 raise Exception("bos client is None!")
130 if not (bucket_name and super_object_key):
131 raise Exception("bucket or object is invalid!")
132 if not os.path.isfile(super_file):
133 raise Exception("source file is invalid!")
134 if not isinstance(cipher_rsa,CipherWithRSA):
135 raise Exception("cipher_rsa is invalid!")
136 except Exception as e:
137 print e
138 exit()
139 if not part_size:
140 part_size = 10*1024*1024
141 #temp_file is the encrypted file part
142 temp_file = _random_string(20)
143 cipher_aes = CipherWithAES()
144 #2. Multipart upload
145 #Multipart upload has three steps. The first step is initialization to obtain upload_id
146 upload_id = bos_client.initiate_multipart_upload(
147 bucket_name = bucket_name,
148 key = super_object_key,
149 ).upload_id
150 left_size = os.path.getsize(super_file)
151 offset = 0
152 part_number = 1
153 part_list = []
154 fs = open(super_file,"r")
155 while left_size > 0:
156 if left_size < part_size:
157 part_size = left_size
158# Read parts and encrypt them
159 part_content = fs.read(part_size)
160 encrypted_part_content = cipher_aes.encrypt(part_content)
161 #Rewrite the encrypted parts into temp_file
162 with open(temp_file,"w") as ft:
163 ft.write(encrypted_part_content)
164 #The second step of multipart upload: upload parts
165 response = bos_client.upload_part_from_file(
166 bucket_name, super_object_key, upload_id, part_number, part_size, temp_file, offset)
167 left_size -= part_size
168 #Save part number and etag for calling complete_multipart_upload()
169 part_list.append({
170 "partNumber": part_number,
171 "eTag": response.metadata.etag
172 })
173 part_number += 1
174 os.remove(temp_file)
175 fs.close()
176 #Use the public key to encrypt the AES key and use it as the object’s meta
177 user_metadata = {
178 "key": base64.b64encode(cipher_rsa.encrypt(str(cipher_aes.key))),
179 "start":base64.b64encode(cipher_rsa.encrypt(str(cipher_aes.start)))
180 }
181 #The third step of multipart upload: complete the multipart upload
182 return bos_client.complete_multipart_upload(bucket_name, super_object_key, upload_id, part_list,user_metadata = user_metadata)
183
184####################### Get super file#######################
185 #Obtain partsof large files, where range_start and range_end are the start and end positions of the part to be obtained in the file, including the byte at the end position
186def get_part_super_file(bos_client, bucket_name, super_object_key, range_start, range_end,cipher_rsa):
187
188 """
189 Get part of super file from baidu object storage
190
191 :param bos_client:bos client
192 :type bos_client:baidubce.services.bos.bos_client.BosClient
193
194 :param bucket_name:None
195 :type bucket_name:string
196
197 :param super_object_key: destion object key of super file
198 :type super_object_key: string
199
200 :param range_start: index of first bytes of part,minimum value is 0
201 :type super_file:int
202
203 :param range_end: index of last bytes of part
204 :type: int
205
206 :param cipher_rsa: dencrypt symmetric key
207 :type cipher_rsa: CipherWithRSA
208
209 :return :**Http Response**
210
211 """
212
213 try:
214 if not isinstance(bos_client,BosClient):
215 raise Exception("bos client is None!")
216 if not (bucket_name and super_object_key):
217 raise Exception("bucket or object is invalid!")
218 if not (range_start and range_end):
219 raise Exception("range is invalid!")
220 if not isinstance(cipher_rsa,CipherWithRSA):
221 raise Exception("cipher_rsa is invalid!")
222 except Exception as e:
223 print e
224 exit()
225 #1. Align the starting position of the part to an integer multiple of 16 bytes
226 left_offset = range_start%16
227 right_offset = 15 -range_end%16
228 test_range = [range_start-left_offset,range_end+right_offset]
229 #2. Obtain the object’s meta
230 response = bos_client.get_object_meta_data(bucket_name, super_object_key)
231 #Use the local public key to decrypt the AES key ciphertext to get the AES key plaintext
232 download_aes_key = base64.b64decode(getattr(response.metadata,"bce_meta_key"))
233 download_aes_start = base64.b64decode(str(getattr(response.metadata,"bce_meta_start")))
234 aes_key = cipher_rsa.decrypt(download_aes_key)
235 aes_start = cipher_rsa.decrypt(download_aes_start)
236 #Adjust the initial value of the counter according to the starting position of the part to be obtained
237 offset_start = int(aes_start)+range_start/16
238 cipher_aes = CipherWithAES(aes_key,int(offset_start))
239 #3. Download the part ciphertext data and decrypt it using the AES key
240 response = bos_client.get_object(bucket_name, super_object_key,test_range)
241 download_content = response.data.read()
242 plaintext_content = cipher_aes.decrypt(download_content)
243 #Intercept the plaintext data of the user-specified segment and return it
244 return plaintext_content[left_offset:range_end-range_start+left_offset+1]
245
246############# put common file ####################
247 #Encrypt and upload ordinary files
248def put_common_file(bos_client, bucket_name, object_key, file_name, cipher_rsa):
249 """
250 Put file to baidu object storage
251
252 :param bos_client:bos client
253 :type bos_client:baidubce.services.bos.bos_client.BosClient
254
255 :param bucket_name:None
256 :type bucket_name:string
257
258 :param object_key: destion object key of file
259 :type object_key: string
260
261 :param file_name: source file name
262 :type file_name:string
263
264 :param cipher_rsa: encrypt symmetric key
265 :type cipher_rsa: CipherWithRSA
266
267 :return :**Http Response**
268
269 """
270
271 try:
272 if not isinstance(bos_client,BosClient):
273 raise Exception("bos client is None!")
274 if not (bucket_name and object_key):
275 raise Exception("bucket or object is invalid!")
276 if not os.path.isfile(file_name):
277 raise Exception("file name is invalid!")
278 if not isinstance(cipher_rsa,CipherWithRSA):
279 raise Exception("cipher_rsa is invalid!")
280 except Exception as e:
281 print e
282 exit()
283 temp_file = _random_string(20)
284 #Read the data of the file to be uploaded
285 content=""
286 with open(file_name,"r") as fp:
287 content = fp.read()
288 cipher_aes = CipherWithAES()
289 #Encrypt the data and write it to temp_file
290 encrypt_content = cipher_aes.encrypt(content)
291 with open(temp_file,"w") as ft:
292 ft.write(encrypt_content)
293 cipher_rsa = CipherWithRSA()
294 #Encrypt the AES key and use it as the object’s meta
295 user_metadata = {
296 "key": base64.b64encode(cipher_rsa.encrypt(str(cipher_aes.key))),
297 "start":base64.b64encode(cipher_rsa.encrypt(str(cipher_aes.start)))
298 }
299 #Upload the encrypted file
300 response = bos_client.put_object_from_file(bucket = bucket_name,
301 key = object_key,
302 file_name = temp_file,
303 user_metadata = user_metadata)
304 os.remove(temp_file)
305 return response
306
307################get common file#####################
308 #Download the encrypted ordinary file
309def get_common_file(bos_client, bucket_name, object_key, des_file, cipher_rsa):
310
311 """
312 Put file to baidu object storage
313
314 :param bos_client:bos client
315 :type bos_client:baidubce.services.bos.bos_client.BosClient
316
317 :param bucket_name:None
318 :type bucket_name:string
319
320 :param object_key: destion object key of file
321 :type object_key: string
322
323 :param des_file: destination file name
324 :type des_file:string
325
326 :param cipher_rsa: dencrypt symmetric key
327 :type cipher_rsa: CipherWithRSA
328
329 :return :**Http Response**
330
331 """
332 try:
333 if not isinstance(bos_client,BosClient):
334 raise Exception("bos client is None!")
335 if not (bucket_name and object_key):
336 raise Exception("bucket or object is invalid!")
337 if not des_file:
338 raise Exception("destination file is invalid!")
339 if not isinstance(cipher_rsa,CipherWithRSA):
340 raise Exception("cipher_rsa is invalid!")
341 except Exception as e:
342 print e
343 exit()
344 #Download to obtain meta
345 response = bos_client.get_object_meta_data(bucket_name, object_key)
346 download_aes_key = base64.b64decode(getattr(response.metadata,"bce_meta_key"))
347 download_aes_start = base64.b64decode(str(getattr(response.metadata,"bce_meta_start")))
348 #Download the encrypted data to the local
349 download_content = bos_client.get_object_as_string(bucket_name, object_key)
350 #Decrypt to get the AES key plaintext
351 aes_key = cipher_rsa.decrypt(download_aes_key)
352 aes_start = cipher_rsa.decrypt(download_aes_start)
353
354 cipher_aes = CipherWithAES(aes_key,int(aes_start))
355 plaintext_content = cipher_aes.decrypt(download_content)
356 with open(des_file,"w") as fd:
357 fd.write(plaintext_content)
358
359if __name__ == "__main__":
360 #Take the object storage in the Beijing region as an example, replace AK, SK, and bucket_name with the user’s data
361 HOST = 'bj.bcebos.com'
362 AK = 'Your_Access_Key'
363 SK = 'Your_Secret_Access_Key'
364 bucket_name = "Your-Bucket-Name"
365 super_file= "super_file"
366 super_object_key = "my-super-object"
367 #Obtain the bos client
368 config = BceClientConfiguration(credentials=BceCredentials(AK, SK), endpoint=HOST)
369 bos_client = BosClient(config)
370
371 #1.1 Upload large files
372 #Set the part size
373 part_size = 10*1024*1024
374 cipher_rsa = CipherWithRSA()
375 #Upload large files in parts
376 put_super_file(bos_client, bucket_name, super_object_key, super_file, part_size,cipher_rsa)
377 #1.2 Obtain large file parts
378 #Set the starting position of the part to be obtained, it is recommended to set it to an integer multiple of 16 bytes
379 range_start = 16*10
380 range_end = 16*11-1
381 #Obtain the plaintext data part
382 result = get_part_super_file(bos_client, bucket_name, super_object_key, range_start, range_end,cipher_rsa)
383 print "#"*20
384 print result
385 print "#"*20
386 print "length:",len(result)
387
388 #2.1 Upload ordinary files
389 object_key = "myobject"
390 source_file = "myobject.txt"
391 put_common_file(bos_client,bucket_name,object_key,source_file,cipher_rsa)
392 #2.2 Download ordinary files
393 des_file = "des_myobject.txt"
394 get_common_file(bos_client,bucket_name,object_key,des_file,cipher_rsa)
395 object_key = "myobject"
396source_file = "myobject.txt"
397put_common_file(bos_client,bucket_name,object_key,source_file,cipher_rsa)
398#2.2 Download ordinary files
399 des_file = "des_myobject.txt"
400get_common_file(bos_client,bucket_name,object_key,des_file,cipher_rsa)
