Mã hóa và giải mã bằng PyCrypto AES 256


171

Tôi đang cố gắng xây dựng hai chức năng bằng PyCrypto chấp nhận hai tham số: tin nhắn và khóa, sau đó mã hóa / giải mã tin nhắn.

Tôi đã tìm thấy một số liên kết trên web để giúp tôi, nhưng mỗi liên kết đều có sai sót:

Cái này ở codekoala sử dụng os.urandom, được PyCrypto không khuyến khích.

Hơn nữa, khóa tôi cung cấp cho chức năng không được đảm bảo có độ dài chính xác như mong đợi. Tôi có thể làm gì để điều đó xảy ra?

Ngoài ra, có một số chế độ, cái nào được khuyến nghị? Tôi không biết sử dụng cái gì: /

Cuối cùng, IV chính xác là gì? Tôi có thể cung cấp IV khác để mã hóa và giải mã, hoặc điều này sẽ trả về kết quả khác?

Chỉnh sửa : Đã xóa phần mã vì nó không an toàn.


12
os.urandom được khuyến khích trên trang web PyCrypto . Nó sử dụng chức năng CryptGenRandom của Microsoft, đó là CSPRNG
Joel Vroom

5
hoặc /dev/urandomtrên Unix
Joel Vroom

2
Chỉ cần làm rõ, trong ví dụ này, cụm mật khẩukhóa có thể là 128, 192 hoặc 256 bit (16, 24 hoặc 32 byte)
Đánh dấu

4
Có thể đáng để đề cập rằng PyCrypto là một dự án đã chết . Cam kết cuối cùng là từ năm 2014. PyCryptodome trông giống như một sự thay thế thả vào tốt
Overdrivr

1
Câu hỏi này đã cũ, nhưng tôi muốn chỉ ra (kể từ năm 2020) rằng pycrypto có thể đã lỗi thời và không còn được hỗ trợ. Nhìn vào trang github của họ ( github.com/pycrypto/pycrypto ), có vẻ như cam kết cuối cùng của họ là vào năm 2014. Tôi không hài lòng khi sử dụng phần mềm mật mã không còn được phát triển
Khó chịu_phd_syndrom

Câu trả lời:


151

Dưới đây là triển khai của tôi và hoạt động với tôi với một số bản sửa lỗi và tăng cường sự liên kết của khóa và cụm từ bí mật với 32 byte và iv đến 16 byte:

import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES

class AESCipher(object):

    def __init__(self, key): 
        self.bs = AES.block_size
        self.key = hashlib.sha256(key.encode()).digest()

    def encrypt(self, raw):
        raw = self._pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.b64encode(iv + cipher.encrypt(raw.encode()))

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s)-1:])]

14
Tôi biết điều này đã được một thời gian nhưng tôi nghĩ rằng phản ứng này có thể lan truyền một số nhầm lẫn. Hàm này sử dụng block_size 32 byte (256 byte) để đệm dữ liệu đầu vào nhưng AES sử dụng kích thước khối 128 bit. Trong AES256, khóa là 256 bit, nhưng không phải kích thước khối.
Tannin

13
để đặt nó theo một cách khác, "self.bs" nên được xóa và thay thế bằng "AES.block_size"
Alexis

2
Tại sao bạn băm chìa khóa? Nếu bạn đang mong đợi rằng đây là một cái gì đó giống như mật khẩu, thì bạn không nên sử dụng SHA256; tốt hơn để sử dụng chức năng phái sinh chính, như PBKDF2, mà PyCrypto cung cấp.
chỉnh sửa

5
@Chris - SHA256 đưa ra hàm băm 32 byte - một khóa có kích thước hoàn hảo cho AES256. Việc tạo / tạo ra một khóa được coi là ngẫu nhiên / an toàn và nằm ngoài phạm vi của mã hóa / giải mã - băm chỉ là một đảm bảo rằng khóa có thể sử dụng được với mật mã được chọn.
zwer

2
trong _pad self.bs là cần thiết và trong _unpad không cần
mnothic

149

Bạn có thể cần hai chức năng sau: pad- để đệm (khi thực hiện mã hóa) và unpad- để hủy bàn phím (khi thực hiện giải mã) khi độ dài của đầu vào không phải là bội số của BLOCK_SIZE.

BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
unpad = lambda s : s[:-ord(s[len(s)-1:])]

Vì vậy, bạn đang hỏi độ dài của phím? Bạn có thể sử dụng md5sum của khóa thay vì sử dụng trực tiếp.

Hơn nữa, theo kinh nghiệm ít ỏi của tôi về việc sử dụng PyCrypto, IV được sử dụng để trộn lẫn đầu ra của mã hóa khi đầu vào giống nhau, vì vậy IV được chọn làm một chuỗi ngẫu nhiên và sử dụng nó làm một phần của đầu ra mã hóa, sau đó sử dụng nó để giải mã tin nhắn.

Và đây là triển khai của tôi, hy vọng nó sẽ hữu ích cho bạn:

import base64
from Crypto.Cipher import AES
from Crypto import Random

class AESCipher:
    def __init__( self, key ):
        self.key = key

    def encrypt( self, raw ):
        raw = pad(raw)
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key, AES.MODE_CBC, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) ) 

    def decrypt( self, enc ):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv )
        return unpad(cipher.decrypt( enc[16:] ))

1
Điều gì xảy ra nếu bạn có đầu vào chính xác là bội số của BLOCK_SIZE? Tôi nghĩ rằng chức năng unpad sẽ có một chút nhầm lẫn ...
Kjir

2
@Kjir, sau đó một chuỗi giá trị chr (BS) có độ dài BLOCK_SIZE sẽ được thêm vào dữ liệu gốc.
Marcus

1
@Marcus padchức năng bị hỏng (ít nhất là trong Py3), thay thế s[:-ord(s[len(s)-1:])]cho nó để hoạt động trên các phiên bản.
Torxed

2
Chức năng pad @Torxed có sẵn trong CryptoUtil.Padding.pad () với pycryptodome (theo dõi pycrypto)
comte

2
Tại sao không chỉ có một ký tự không đổi là char đệm?
Inaimathi

16

Hãy để tôi giải quyết câu hỏi của bạn về "chế độ." AES256 là một loại mật mã khối . Phải mất như là đầu vào 32-byte chìa khóa và một chuỗi 16 byte, được gọi là khối và kết quả đầu ra một khối. Chúng tôi sử dụng AES trong chế độ hoạt động để mã hóa. Các giải pháp trên đề xuất sử dụng CBC, đây là một ví dụ. Một cái khác được gọi là CTR và nó có phần dễ sử dụng hơn:

from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto import Random

# AES supports multiple key sizes: 16 (AES128), 24 (AES192), or 32 (AES256).
key_bytes = 32

# Takes as input a 32-byte key and an arbitrary-length plaintext and returns a
# pair (iv, ciphtertext). "iv" stands for initialization vector.
def encrypt(key, plaintext):
    assert len(key) == key_bytes

    # Choose a random, 16-byte IV.
    iv = Random.new().read(AES.block_size)

    # Convert the IV to a Python integer.
    iv_int = int(binascii.hexlify(iv), 16) 

    # Create a new Counter object with IV = iv_int.
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)

    # Create AES-CTR cipher.
    aes = AES.new(key, AES.MODE_CTR, counter=ctr)

    # Encrypt and return IV and ciphertext.
    ciphertext = aes.encrypt(plaintext)
    return (iv, ciphertext)

# Takes as input a 32-byte key, a 16-byte IV, and a ciphertext, and outputs the
# corresponding plaintext.
def decrypt(key, iv, ciphertext):
    assert len(key) == key_bytes

    # Initialize counter for decryption. iv should be the same as the output of
    # encrypt().
    iv_int = int(iv.encode('hex'), 16) 
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)

    # Create AES-CTR cipher.
    aes = AES.new(key, AES.MODE_CTR, counter=ctr)

    # Decrypt and return the plaintext.
    plaintext = aes.decrypt(ciphertext)
    return plaintext

(iv, ciphertext) = encrypt(key, 'hella')
print decrypt(key, iv, ciphertext)

Điều này thường được gọi là AES-CTR. Tôi sẽ khuyên bạn nên thận trọng khi sử dụng AES-CBC với PyCrypto . Lý do là nó yêu cầu bạn chỉ định sơ đồ đệm , như được minh họa bằng các giải pháp khác được đưa ra. Nói chung, nếu bạn không phải rất cẩn thận về đệm, các cuộc tấn công mà hoàn toàn phá vỡ mã hóa!

Bây giờ, điều quan trọng cần lưu ý là khóa phải là một chuỗi 32 byte ngẫu nhiên ; một mật khẩu không đủ. Thông thường, khóa được tạo như vậy:

# Nominal way to generate a fresh key. This calls the system's random number
# generator (RNG).
key1 = Random.new().read(key_bytes)

Một khóa cũng có thể được lấy từ một mật khẩu :

# It's also possible to derive a key from a password, but it's important that
# the password have high entropy, meaning difficult to predict.
password = "This is a rather weak password."

# For added # security, we add a "salt", which increases the entropy.
#
# In this example, we use the same RNG to produce the salt that we used to
# produce key1.
salt_bytes = 8 
salt = Random.new().read(salt_bytes)

# Stands for "Password-based key derivation function 2"
key2 = PBKDF2(password, salt, key_bytes)

Một số giải pháp ở trên đề xuất sử dụng SHA256 để lấy khóa, nhưng điều này thường được coi là thực hành mật mã xấu . Kiểm tra wikipedia để biết thêm về các chế độ hoạt động.


iv_int = int (binascii.hexlify (iv), 16) không hoạt động, thay thế nó bằng iv_int = int (binascii.hexlify (iv), 16) cộng với 'nhập binascii' và nó sẽ hoạt động (trên Python 3.x ), nếu không thì công việc tuyệt vời!
Valmond

Lưu ý rằng tốt hơn là sử dụng các chế độ Mã hóa Autehnticated như AES-GCM. GCM sử dụng chế độ CTR trong nội bộ.
kelalaka

Mã này khiến "LoạiError: Loại đối tượng <class 'str'> không thể được chuyển sang mã C"
Da Woon Jung

7

Đối với ai đó muốn sử dụng urlsafe_b64encode và urlsafe_b64decode, đây là phiên bản phù hợp với tôi (sau khi dành một chút thời gian với vấn đề unicode)

BS = 16
key = hashlib.md5(settings.SECRET_KEY).hexdigest()[:BS]
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[:-ord(s[len(s)-1:])]

class AESCipher:
    def __init__(self, key):
        self.key = key

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.urlsafe_b64encode(iv + cipher.encrypt(raw)) 

    def decrypt(self, enc):
        enc = base64.urlsafe_b64decode(enc.encode('utf-8'))
        iv = enc[:BS]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[BS:]))

6

Bạn có thể lấy một cụm mật khẩu từ một mật khẩu tùy ý bằng cách sử dụng hàm băm mật mã ( KHÔNG phải phần mềm dựng sẵn của Python hash) như SHA-1 hoặc SHA-256. Python bao gồm hỗ trợ cho cả hai trong thư viện tiêu chuẩn của nó:

import hashlib

hashlib.sha1("this is my awesome password").digest() # => a 20 byte string
hashlib.sha256("another awesome password").digest() # => a 32 byte string

Bạn có thể cắt bớt một giá trị băm mật mã chỉ bằng cách sử dụng [:16]hoặc [:24]nó sẽ giữ được bảo mật của nó theo độ dài bạn chỉ định.


13
Bạn không nên sử dụng hàm băm gia đình SHA để tạo khóa từ mật khẩu - xem bài tiểu luận của Coda Hale về chủ đề này . Thay vào đó, hãy xem xét sử dụng chức năng phái sinh khóa thực như mã hóa . (Bài tiểu luận của Coda Hale đã được viết trước khi xuất bản của tiền điện tử.)
Benjamin Barenblat

7
Đối với những người đọc trong tương lai, nếu bạn đang tìm cách lấy một khóa từ cụm mật khẩu, hãy tìm PBKDF2. Nó khá dễ sử dụng trong python ( pypi.python.org/pypi/pbkdf2 ). Tuy nhiên, nếu bạn đang tìm cách băm mật khẩu, bcrypt là một lựa chọn tốt hơn.
C Fairweather

6

Biết ơn những câu trả lời khác đã truyền cảm hứng nhưng không hiệu quả với tôi.

Sau khi chi tiêu giờ cố gắng tìm ra cách thức hoạt động, tôi đã đưa ra với việc thực hiện dưới đây với các mới nhất PyCryptodomex thư viện (nó là một câu chuyện làm thế nào tôi quản lý để thiết lập nó đằng sau proxy, trên Windows, trong một virtualenv .. phew)

làm việc trên việc thực hiện của bạn, hãy nhớ ghi lại phần đệm, mã hóa, mã hóa các bước (và ngược lại). Bạn phải đóng gói và giải nén theo thứ tự.

cơ sở nhập khẩu64
nhập khẩu hashlib
từ Cryptodome. Nhập mã AES
từ Cryptodome.Random nhập get_random_bytes

__key__ = hashlib.sha256 (khóa b'16 ký tự '). digest ()

mã hóa def (thô):
    BS = AES.block_size
    pad = lambda s: s + (BS - len (s)% BS) * chr (BS - len (s)% BS)

    raw = base64.b64encode (pad (raw) .encode ('utf8'))
    iv = get_random_bytes (AES.block_size)
    mật mã = ​​AES.new (key = __key__, mode = AES.MODE_CFB, iv = iv)
    trả về base64.b64encode (iv + Codes.encrypt (raw))

giải mã def (enc):
    unpad = lambda s: s [: - ord (s [-1:])]

    enc = base64.b64decode (enc)
    iv = enc [: AES.block_size]
    mật mã = ​​AES.new (__ key__, AES.MODE_CFB, iv)
    trả về unpad (base64.b64decode (cext.decrypt (enc [AES.block_size:])). decode ('utf8'))

Cảm ơn bạn vui lòng cho một ví dụ hoạt động của điều này với libs PyCryptodomeX. Điều đó khá hữu ích!
Ygramul

5

Vì lợi ích của người khác, đây là cách triển khai giải mã mà tôi có được bằng cách kết hợp các câu trả lời của @Cyril và @Marcus. Điều này giả định rằng điều này xuất hiện thông qua Yêu cầu HTTP với mã hóa được trích dẫn và mã hóa base64.

import base64
import urllib2
from Crypto.Cipher import AES


def decrypt(quotedEncodedEncrypted):
    key = 'SecretKey'

    encodedEncrypted = urllib2.unquote(quotedEncodedEncrypted)

    cipher = AES.new(key)
    decrypted = cipher.decrypt(base64.b64decode(encodedEncrypted))[:16]

    for i in range(1, len(base64.b64decode(encodedEncrypted))/16):
        cipher = AES.new(key, AES.MODE_CBC, base64.b64decode(encodedEncrypted)[(i-1)*16:i*16])
        decrypted += cipher.decrypt(base64.b64decode(encodedEncrypted)[i*16:])[:16]

    return decrypted.strip()

5

Một người khác đảm nhận việc này (xuất phát rất nhiều từ các giải pháp ở trên) nhưng

  • sử dụng null để đệm
  • không sử dụng lambda (chưa bao giờ là fan hâm mộ)
  • thử nghiệm với python 2.7 và 3.6.5

    #!/usr/bin/python2.7
    # you'll have to adjust for your setup, e.g., #!/usr/bin/python3
    
    
    import base64, re
    from Crypto.Cipher import AES
    from Crypto import Random
    from django.conf import settings
    
    class AESCipher:
        """
          Usage:
          aes = AESCipher( settings.SECRET_KEY[:16], 32)
          encryp_msg = aes.encrypt( 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppp' )
          msg = aes.decrypt( encryp_msg )
          print("'{}'".format(msg))
        """
        def __init__(self, key, blk_sz):
            self.key = key
            self.blk_sz = blk_sz
    
        def encrypt( self, raw ):
            if raw is None or len(raw) == 0:
                raise NameError("No value given to encrypt")
            raw = raw + '\0' * (self.blk_sz - len(raw) % self.blk_sz)
            raw = raw.encode('utf-8')
            iv = Random.new().read( AES.block_size )
            cipher = AES.new( self.key.encode('utf-8'), AES.MODE_CBC, iv )
            return base64.b64encode( iv + cipher.encrypt( raw ) ).decode('utf-8')
    
        def decrypt( self, enc ):
            if enc is None or len(enc) == 0:
                raise NameError("No value given to decrypt")
            enc = base64.b64decode(enc)
            iv = enc[:16]
            cipher = AES.new(self.key.encode('utf-8'), AES.MODE_CBC, iv )
            return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf-8')

Điều này sẽ không hoạt động nếu byte đầu vào [] có null nulling bởi vì trong hàm decrypt () bạn sẽ ăn null null đệm của mình PLUS bất kỳ null null nào.
Buzz Moschetti

Vâng, như tôi nêu ở trên, phần logic này có null. Nếu các mục bạn muốn mã hóa / giải mã có thể có null null, tốt hơn là sử dụng một trong các giải pháp khác tại đây
MIkee

2

Hơi muộn một chút nhưng tôi nghĩ nó sẽ rất hữu ích. Không ai đề cập đến sơ đồ sử dụng như phần đệm PKCS # 7. Bạn có thể sử dụng nó thay vì các chức năng trước đó để đệm (khi thực hiện mã hóa) và unpad (khi thực hiện giải mã) .i sẽ cung cấp Mã nguồn đầy đủ bên dưới.

import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
import pkcs7
class Encryption:

    def __init__(self):
        pass

    def Encrypt(self, PlainText, SecurePassword):
        pw_encode = SecurePassword.encode('utf-8')
        text_encode = PlainText.encode('utf-8')

        key = hashlib.sha256(pw_encode).digest()
        iv = Random.new().read(AES.block_size)

        cipher = AES.new(key, AES.MODE_CBC, iv)
        pad_text = pkcs7.encode(text_encode)
        msg = iv + cipher.encrypt(pad_text)

        EncodeMsg = base64.b64encode(msg)
        return EncodeMsg

    def Decrypt(self, Encrypted, SecurePassword):
        decodbase64 = base64.b64decode(Encrypted.decode("utf-8"))
        pw_encode = SecurePassword.decode('utf-8')

        iv = decodbase64[:AES.block_size]
        key = hashlib.sha256(pw_encode).digest()

        cipher = AES.new(key, AES.MODE_CBC, iv)
        msg = cipher.decrypt(decodbase64[AES.block_size:])
        pad_text = pkcs7.decode(msg)

        decryptedString = pad_text.decode('utf-8')
        return decryptedString

import StringIO
import binascii


def decode(text, k=16):
    nl = len(text)
    val = int(binascii.hexlify(text[-1]), 16)
    if val > k:
        raise ValueError('Input is not padded or padding is corrupt')

    l = nl - val
    return text[:l]


def encode(text, k=16):
    l = len(text)
    output = StringIO.StringIO()
    val = k - (l % k)
    for _ in xrange(val):
        output.write('%02x' % val)
    return text + binascii.unhexlify(output.getvalue())


Tôi không biết ai đã đánh giá thấp câu trả lời nhưng tôi tò mò muốn biết tại sao. Có lẽ phương pháp này không an toàn? Một lời giải thích sẽ là tuyệt vời.
Cyril N.

1
@CyrilN. Câu trả lời này cho thấy băm mật khẩu với một lệnh gọi SHA-256 là đủ. Không phải vậy. Bạn thực sự nên sử dụng PBKDF2 hoặc tương tự cho việc tạo khóa từ mật khẩu bằng cách sử dụng số lần lặp lớn.
Artjom B.

Cảm ơn bạn đã biết chi tiết @ArtjomB.!
Cyril N.

Tôi có một chìa khóa và cũng là phím iv với chiều dài 44. Làm thế nào tôi có thể sử dụng chức năng của bạn?! tất cả các thuật toán trên internet mà tôi tìm thấy, có vấn đề với độ dài của khóa vectơ của tôi
mahshid.r


2

Tôi đã sử dụng cả hai CryptoPyCryptodomexthư viện và nó được blazing nhanh ...

import base64
import hashlib
from Cryptodome.Cipher import AES as domeAES
from Cryptodome.Random import get_random_bytes
from Crypto import Random
from Crypto.Cipher import AES as cryptoAES

BLOCK_SIZE = AES.block_size

key = "my_secret_key".encode()
__key__ = hashlib.sha256(key).digest()
print(__key__)

def encrypt(raw):
    BS = cryptoAES.block_size
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    raw = base64.b64encode(pad(raw).encode('utf8'))
    iv = get_random_bytes(cryptoAES.block_size)
    cipher = cryptoAES.new(key= __key__, mode= cryptoAES.MODE_CFB,iv= iv)
    a= base64.b64encode(iv + cipher.encrypt(raw))
    IV = Random.new().read(BLOCK_SIZE)
    aes = domeAES.new(__key__, domeAES.MODE_CFB, IV)
    b = base64.b64encode(IV + aes.encrypt(a))
    return b

def decrypt(enc):
    passphrase = __key__
    encrypted = base64.b64decode(enc)
    IV = encrypted[:BLOCK_SIZE]
    aes = domeAES.new(passphrase, domeAES.MODE_CFB, IV)
    enc = aes.decrypt(encrypted[BLOCK_SIZE:])
    unpad = lambda s: s[:-ord(s[-1:])]
    enc = base64.b64decode(enc)
    iv = enc[:cryptoAES.block_size]
    cipher = cryptoAES.new(__key__, cryptoAES.MODE_CFB, iv)
    b=  unpad(base64.b64decode(cipher.decrypt(enc[cryptoAES.block_size:])).decode('utf8'))
    return b

encrypted_data =encrypt("Hi Steven!!!!!")
print(encrypted_data)
print("=======")
decrypted_data = decrypt(encrypted_data)
print(decrypted_data)

1
from Crypto import Random
from Crypto.Cipher import AES
import base64

BLOCK_SIZE=16
def trans(key):
     return md5.new(key).digest()

def encrypt(message, passphrase):
    passphrase = trans(passphrase)
    IV = Random.new().read(BLOCK_SIZE)
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return base64.b64encode(IV + aes.encrypt(message))

def decrypt(encrypted, passphrase):
    passphrase = trans(passphrase)
    encrypted = base64.b64decode(encrypted)
    IV = encrypted[:BLOCK_SIZE]
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return aes.decrypt(encrypted[BLOCK_SIZE:])

10
Vui lòng cung cấp không chỉ mã mà còn giải thích những gì bạn đang làm và tại sao điều này tốt hơn / sự khác biệt với câu trả lời hiện có.
Florian Koch

Thay thế md5.new (key) .digest () bằng md5 (key) .digest () và nó hoạt động như một bùa mê!
Một STEFani
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.