Tải bản đầy đủ - 0 (trang)
9 Using Salts, Nonces, and Initialization Vectors

9 Using Salts, Nonces, and Initialization Vectors

Tải bản đầy đủ - 0trang

The use of salt means that the attacker would have to produce a totally separate dictionary for every possible salt value. If the salt is big enough, it essentially makes dictionary attacks infeasible. However, the attacker can generally still try to guess every

password without using a stronger protocol. For a discussion of various passwordbased authentication technologies, see Recipe 8.1.

If the salt isn’t chosen at random, certain dictionaries will be more likely than others. For this reason, salt is generally expected to be random.

Salt can be generated using the techniques discussed in Chapter 11.



Nonces

Nonces* are bits of data often input to cryptographic protocols and algorithms,

including many message authentication codes and some encryption modes. Such values should only be used a single time with any particular cryptographic key. In fact,

reuse generally isn’t prohibited, but the odds of reuse need to be exceptionally low.

That is, if you have a nonce that is very large compared to the number of times you

expect to use it (e.g., the nonce is 128 bits, and you don’t expect to use it more than

232 times), it is sufficient to choose nonces using a cryptographically strong pseudorandom number generator.

Sequential nonces have a few advantages over random nonces:

• You can easily guarantee that nonces are not repeated. Note, though, that if the

possible nonce space is large, this is not a big concern.

• Many protocols already send a unique sequence number for each packet, so one

can save space in transmitted messages.

• The sequential ordering of nonces can be used to prevent replay attacks, but

only if you actually check to ensure that the nonce is always incrementing. That

is, if each message has a nonce attached to it, you can tell whether the message

came in the right order, by looking at the nonce and making sure its value is

always incrementing.

However, randomness in a nonce helps prevent against classes of attacks that amortize work across multiple keys in the same system.

We recommend that nonces have both a random portion and a sequential portion.

Generally, the most significant bytes should be random, and the final 6 to 8 bytes

should be sequential. An 8-byte counter can accommodate 264 messages without the

counter’s repeating, which should be more than big enough for any system.

If you use both a nonce and a salt, you can select a single random part for each key

you use. The nonce on the whole has to be unique, but the salt can remain fixed for

* In the UK, “nonce” is slang for a child sex offender. However, this term is widespread in the cryptographic

world, so we use it.



134



|



Chapter 4: Symmetric Cryptography Fundamentals

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



the lifetime of the key; the counter ensures that the nonce is always unique. In such a

nonce, the random part is said to be a “salt.” Generally, it’s good to have four or

more bytes of salt in a nonce.

If you decide to use only a random nonce, remember that the nonce needs to be

changed after each message, and you lose the ability to prevent against capturereplay attacks.

The random portion of a nonce can be generated using the techniques discussed in

Chapter 11. Generally, you will have a fixed-size buffer into which you place the

nonce, and you will then set the remaining bytes to zero, incrementing them after

each message is sent. For example, if you have a 16-byte nonce with an 8-byte

counter in the least significant bytes, you might use the following code:

/* This assumes a 16-byte nonce where the last 8 bytes represent the counter! */

void increment_nonce(unsigned char *nonce) {

if (!++nonce[15]) if (!++nonce[14]) if (!++nonce[13]) if (!++nonce[12])

if (!++nonce[11]) if (!++nonce[10]) if (!++nonce[9]) if (!++nonce[8]) {

/* If you get here, you're out of nonces. This really shouldn't happen

* with an 8-byte nonce, so often you'll see: if (!++nonce[9]) ++nonce[8];

*/

}

}



Note that the this code can be more efficient if we do a 32-bit increment, but then

there are endian-ness issues that make portability more difficult.

If sequential nonces are implemented correctly, they can help thwart

capture relay attacks (see Recipe 6.1).



Initialization vectors (IVs)

The term initialization vector (IV) is the most widely used and abused of the three

terms we’ve been discussing. IV and nonce are often used interchangeably. However, a careful definition does differentiate between these two concepts. For our purposes, an IV is a nonce with an additional requirement: it must be selected in a

nonpredictable way. That is, the IV can’t be sequential; it must be random. One popular example in which a real IV is required for maximizing security is when using the

CBC encryption mode (see Recipe 5.6).

The big downside to an IV, as compared to a nonce, is that an IV does not afford

protection against capture-replay attacks—unless you’re willing to remember every

IV that has ever been used, which is not a good solution. To ensure protection

against such attacks when using an IV, the higher-level protocol must have its own

notion of sequence numbers that get checked in order.



Using Salts, Nonces, and Initialization Vectors | 135

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



Another downside is that there is generally more data to send. Systems that use

sequential nonces can often avoid sending the nonce, as it can be calculated from the

sequence number already sent with the message.

Initialization vectors can be generated using the techniques discussed in Chapter 11.



See Also

• Chapter 11

• Recipes 5.6, 6.21, 8.1



4.10 Deriving Symmetric Keys from a Password

Problem

You do not want passwords to be stored on disk. Instead, you would like to convert

a password into a cryptographic key.



Solution

Use PBKDF2, the password-based key derivation function 2, specified in PKCS #5.*

You can also use this recipe to derive keys from other keys. See Recipe

4.1 for considerations; that recipe also discusses considerations for

choosing good salt values.



Discussion

Passwords can generally vary in length, whereas symmetric keys are almost always a

fixed size. Passwords may be vulnerable to guessing attacks, but ultimately we’d prefer symmetric keys not to be as easily guessable.

The function spc_pbkdf2( ) in the following code is an implementation of PKCS #5,

Version 2.0. PKCS #5 stands for “Public Key Cryptography Standard #5,” although

there is nothing public-key-specific about this standard. The standard defines a way

to turn a password into a symmetric key. The name of the function stands for “password-based key derivation function 2,” where the 2 indicates that the function

implements Version 2.0 of PKCS #5.

#include

#include

#include



* This standard is available from RSA Security at http://www.rsasecurity.com/rsalabs/pkcs/pkcs-5/.



136



|



Chapter 4: Symmetric Cryptography Fundamentals

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



#include

#include

#include

#include









/* for htonl */



#ifdef WIN32

typedef unsigned __int64 spc_uint64_t;

#else

typedef unsigned long long spc_uint64_t;

#endif

/* This value needs to be the output size of your pseudo-random function (PRF)! */

#define PRF_OUT_LEN 20

/* This is an implementation of the PKCS#5 PBKDF2 PRF using HMAC-SHA1.

* always gives 20-byte outputs.

*/



It



/* The first three functions are internal helper functions. */

static void pkcs5_initial_prf(unsigned char *p, size_t plen, unsigned char *salt,

size_t saltlen, size_t i, unsigned char *out,

size_t *outlen) {

size_t

swapped_i;

HMAC_CTX

ctx;

HMAC_CTX_init(&ctx);

HMAC_Init(&ctx, p, plen, EVP_sha1( ));

HMAC_Update(&ctx, salt, saltlen);

swapped_i = htonl(i);

HMAC_Update(&ctx, (unsigned char *)&swapped_i, 4);

HMAC_Final(&ctx, out, (unsigned int *)outlen);

}

/* The PRF doesn't *really* change in subsequent calls, but above we handled the

* concatenation of the salt and i within the function, instead of external to it,

* because the implementation is easier that way.

*/

static void pkcs5_subsequent_prf(unsigned char *p, size_t plen, unsigned char *v,

size_t vlen, unsigned char *o, size_t *olen) {

HMAC_CTX ctx;

HMAC_CTX_init(&ctx);

HMAC_Init(&ctx, p, plen, EVP_sha1( ));

HMAC_Update(&ctx, v, vlen);

HMAC_Final(&ctx, o, (unsigned int *)olen);

}

static void pkcs5_F(unsigned char *p, size_t plen, unsigned char *salt,

size_t saltlen, size_t ic, size_t bix, unsigned char *out) {

size_t

i = 1, j, outlen;

unsigned char ulast[PRF_OUT_LEN];

memset(out,0, PRF_OUT_LEN);

pkcs5_initial_prf(p, plen, salt, saltlen, bix, ulast, &outlen);

while (i++ <= ic) {



Deriving Symmetric Keys from a Password | 137

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



for (j = 0; j < PRF_OUT_LEN; j++) out[j] ^= ulast[j];

pkcs5_subsequent_prf(p, plen, ulast, PRF_OUT_LEN, ulast, &outlen);

}

for (j = 0;



j < PRF_OUT_LEN;



j++) out[j] ^= ulast[j];



}

void spc_pbkdf2(unsigned char *pw, unsigned int pwlen, char *salt,

spc_uint64_t saltlen, unsigned int ic, unsigned char *dk,

spc_uint64_t dklen) {

unsigned long i, l, r;

unsigned char final[PRF_OUT_LEN] = {0,};

if (dklen > ((((spc_uint64_t)1) << 32) - 1) * PRF_OUT_LEN) {

/* Call an error handler. */

abort( );

}

l = dklen / PRF_OUT_LEN;

r = dklen % PRF_OUT_LEN;

for (i = 1; i <= l; i++)

pkcs5_F(pw, pwlen, salt, saltlen, ic, i, dk + (i - 1) * PRF_OUT_LEN);

if (r) {

pkcs5_F(pw, pwlen, salt, saltlen, ic, i, final);

for (l = 0; l < r; l++) *(dk + (i - 1) * PRF_OUT_LEN + l) = final[l];

}

}



The spc_pbkdf2( ) function takes seven arguments:

pw



Password, represented as an arbitrary string of bytes.

pwlen



Number of bytes in the password.

salt



String that need not be private but should be unique to the user. The notion of

salt is discussed in Recipe 4.9.

saltlen



Number of bytes in the salt.

ic



“Iteration count,” described in more detail later in this section. A good value is

10,000.

dk



Buffer into which the derived key will be placed.

dklen



Length of the desired derived key in bytes.

The Windows version of spc_pbkdf2( ) is called SpcPBKDF2( ). It has essentially the

same signature, though the names are slightly different because of Windows naming

conventions. The implementation uses CryptoAPI for HMAC-SHA1 and requires

SpcGetExportableContext( ) and SpcImportKeyData( ) from Recipe 5.26.

138



|



Chapter 4: Symmetric Cryptography Fundamentals

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



#include

#include

/* This value needs to be the output size of your pseudo-random function (PRF)! */

#define PRF_OUT_LEN 20

/* This is an implementation of the PKCS#5 PBKDF2 PRF using HMAC-SHA1.

* always gives 20-byte outputs.

*/



It



static HCRYPTHASH InitHMAC(HCRYPTPROV hProvider, HCRYPTKEY hKey, ALG_ID Algid) {

HMAC_INFO HMACInfo;

HCRYPTHASH hHash;

HMACInfo.HashAlgid

= Algid;

HMACInfo.pbInnerString = HMACInfo.pbOuterString = 0;

HMACInfo.cbInnerString = HMACInfo.cbOuterString = 0;

if (!CryptCreateHash(hProvider, CALG_HMAC, hKey, 0, &hHash)) return 0;

CryptSetHashParam(hHash, HP_HMAC_INFO, (BYTE *)&HMACInfo, 0);

return hHash;

}

static void FinalHMAC(HCRYPTHASH hHash, BYTE *pbOut, DWORD *cbOut) {

*cbOut = PRF_OUT_LEN;

CryptGetHashParam(hHash, HP_HASHVAL, pbOut, cbOut, 0);

CryptDestroyHash(hHash);

}

static DWORD SwapInt32(DWORD dwInt32) {

__asm mov

eax, dwInt32

__asm bswap eax

}

static BOOL PKCS5InitialPRF(HCRYPTPROV hProvider, HCRYPTKEY hKey,

BYTE *pbSalt, DWORD cbSalt, DWORD dwCounter,

BYTE *pbOut, DWORD *cbOut) {

HCRYPTHASH hHash;

if (!(hHash = InitHMAC(hProvider, hKey, CALG_SHA1))) return FALSE;

CryptHashData(hHash, pbSalt, cbSalt, 0);

dwCounter = SwapInt32(dwCounter);

CryptHashData(hHash, (BYTE *)&dwCounter, sizeof(dwCounter), 0);

FinalHMAC(hHash, pbOut, cbOut);

return TRUE;

}

static BOOL PKCS5UpdatePRF(HCRYPTPROV hProvider, HCRYPTKEY hKey,

BYTE *pbSalt, DWORD cbSalt,

BYTE *pbOut, DWORD *cbOut) {

HCRYPTHASH hHash;

if (!(hHash = InitHMAC(hProvider, hKey, CALG_SHA1))) return FALSE;

CryptHashData(hHash, pbSalt, cbSalt, 0);

FinalHMAC(hHash, pbOut, cbOut);



Deriving Symmetric Keys from a Password | 139

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



return TRUE;

}

static BOOL PKCS5FinalPRF(HCRYPTPROV hProvider, HCRYPTKEY hKey,

BYTE *pbSalt, DWORD cbSalt, DWORD dwIterations,

DWORD dwBlock, BYTE *pbOut) {

BYTE pbBuffer[PRF_OUT_LEN];

DWORD cbBuffer, dwIndex, dwIteration = 1;

SecureZeroMemory(pbOut, PRF_OUT_LEN);

if (!(PKCS5InitialPRF(hProvider, hKey, pbSalt, cbSalt, dwBlock, pbBuffer,

&cbBuffer))) return FALSE;

while (dwIteration < dwIterations) {

for (dwIndex = 0; dwIndex < PRF_OUT_LEN; dwIndex++)

pbOut[dwIndex] ^= pbBuffer[dwIndex];

if (!(PKCS5UpdatePRF(hProvider, hKey, pbBuffer, PRF_OUT_LEN, pbBuffer,

&cbBuffer))) return FALSE;

}

for (dwIndex = 0; dwIndex < PRF_OUT_LEN; dwIndex++)

pbOut[dwIndex] ^= pbBuffer[dwIndex];

return TRUE;

}

BOOL SpcPBKDF2(BYTE *pbPassword, DWORD cbPassword, BYTE *pbSalt, DWORD cbSalt,

DWORD dwIterations, BYTE *pbOut, DWORD cbOut) {

BOOL

bResult = FALSE;

BYTE

pbFinal[PRF_OUT_LEN];

DWORD

dwBlock, dwBlockCount, dwLeftOver;

HCRYPTKEY hKey;

HCRYPTPROV hProvider;

if (cbOut > ((((__int64)1) << 32) - 1) * PRF_OUT_LEN) return FALSE;

if (!(hProvider = SpcGetExportableContext( ))) return FALSE;

if (!(hKey = SpcImportKeyData(hProvider, CALG_RC4, pbPassword, cbPassword))) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

dwBlockCount = cbOut / PRF_OUT_LEN;

dwLeftOver

= cbOut % PRF_OUT_LEN;

for (dwBlock = 1; dwBlock <= dwBlockCount; dwBlock++) {

if (!PKCS5FinalPRF(hProvider, hKey, pbSalt, cbSalt, dwIterations, dwBlock,

pbOut + (dwBlock - 1) * PRF_OUT_LEN)) goto done;

}

if (dwLeftOver) {

SecureZeroMemory(pbFinal, PRF_OUT_LEN);

if (!PKCS5FinalPRF(hProvider, hKey, pbSalt, cbSalt, dwIterations, dwBlock,

pbFinal)) goto done;

CopyMemory(pbOut + (dwBlock - 1) * PRF_OUT_LEN, pbFinal, dwLeftOver);

}

bResult = TRUE;

done:

CryptDestroyKey(hKey);



140



|



Chapter 4: Symmetric Cryptography Fundamentals

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



CryptReleaseContext(hProvider, hKey);

return bResult;

}



The salt is used to prevent against a dictionary attack. Without salt, a malicious system administrator could easily figure out when a user has the same password as

someone else, and he would be able to precompute a huge dictionary of common

passwords and look to see if the user’s password is in that list.

While salt is not expected to be private, it still must be chosen carefully. See Recipe

4.9 for more on salt.



How Many Iterations?

To what value should you set the iteration count? The answer depends on the environment in which you expect your software to be deployed. The basic idea is to increase

computational costs so that a brute-force attack with lots of high-end hardware is as

expensive as possible, but not to cause too noticeable a delay on the lowest-end box on

which you would wish to run legitimately.

Often, password computations such as these occur on a server. However, there are still

people out there who run servers on their 33 MHz machines. We personally believe

that people running on that kind of hardware should be able to tolerate a one-second

delay, at the very least when computing a password for a modern application. Usually,

a human waiting on the other end will be willing to tolerate an even longer wait as long

as they know why they are waiting. Two to three seconds isn’t so bad.

With that guideline, we have timed our PKCS #5 implementation with some standard input. Based on those timings, we think that 10,000 is good for most applications, and 5,000 is the lowest iteration count you should consider in this day and age.

On a 33 MHz machine, 10,000 iterations should take about 2.5 seconds to process.

On a 1.67 GHz machine, they take a mere 0.045 seconds. Even if your computation

occurs on an embedded processor, people will still be able to tolerate the delay.

The good thing is that it would take a single 1.67 GHz machine more than 6 years to

guess 232 passwords, when using PKCS #5 and 10,000 iterations. Therefore, if there

really is at least 32 bits of entropy in your password (which is very rare), you probably

won’t have to worry about any attacker who has fewer than a hundred high-end

machines at his disposal, at least for a few years.

Expect governments that want your password to put together a few thousand boxes

complete with crypto acceleration, though!



Even with salt, password-guessing attacks are still possible. To prevent against this

kind of attack, PKCS #5 allows the specification of an iteration count, which basically causes an expensive portion of the key derivation function to loop the specified

number of times. The idea is to slow down the time it takes to compute a single key

Deriving Symmetric Keys from a Password | 141

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



from a password. If you make key derivation take a tenth of a second, the user won’t

notice. However, if an attacker tries to carry out an exhaustive search of all possible

passwords, she will have to spend a tenth of a second for each password she wants to

try, which will make cracking even a weak password quite difficult. As we describe in

the sidebar “How Many Iterations?”, we recommend an iteration count of 10,000.

The actual specification of the key derivation function can be found in Version 2.0 of

the PKCS #5 standards document. In brief, we use a pseudo-random function using

the password and salt to get out as many bytes as we need, and we then take those

outputs and feed them back into themselves for each iteration.

There’s no need to use HMAC-SHA1 in PKCS #5. Instead, you could use the

Advanced Encryption Standard (AES) as the underlying cryptographic primitive,

substituting SHA1 for a hash function based on AES (see Recipes 6.15 and 6.16).



See Also

• RSA’s PKCS #5 page: http://www.rsasecurity.com/rsalabs/pkcs/pkcs-5/

• Recipes 4.9, 4.11, 5.26, 6.15, 6.16



4.11 Algorithmically Generating Symmetric Keys

from One Base Secret

Problem

You want to generate a key to use for a short time from a long-term secret (generally

a key, but perhaps a password). If a short-term key is compromised, it should be

impossible to recover the base secret. Multiple entities in the system should be able

to compute the same derived key if they have the right base secret.

For example, you might want to have a single long-term key and use it to create daily

encryption keys or session-specific keys.



Solution

Mix a base secret and any unique information you have available, passing them

through a pseudo-random function (PRF), as discussed in the following section.



Discussion

The basic idea behind secure key derivation is to take a base secret and a unique

identifier that distinguishes the key to be derived (called a distinguisher) and pass

those two items through a pseudo-random function. The PRF acts very much like a



142



|



Chapter 4: Symmetric Cryptography Fundamentals

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



cryptographic one-way hash from a theoretical security point of view, and indeed,

such a one-way hash is often good as a PRF.

There are many different ad hoc solutions for doing key derivation, ranging from the

simple to the complex. On the simple side of the spectrum, you can concatenate a

base key with unique data and pass the string through SHA1. On the complex side is

the PBKDF2 function from PKCS #5 (described in Recipe 4.10).

The simple SHA1 approach is perhaps too simple for general-purpose requirements.

In particular, there are cases where you one might need a key that is larger than the

SHA1 output length (i.e., if you’re using AES with 192-bit keys but are willing to

have only 160 bits of strength). A general-purpose hash function maps n bits to a

fixed number of bits, whereas we would like a function capable of mapping n bits to

m bits.

PBKDF2 can be overkill. Its interface includes functionality to thwart passwordguessing attacks, which is unnecessary when deriving keys from secrets that were

themselves randomly generated.

Fortunately, it is easy to build an n-bit to m-bit PRF that is secure for key derivation.

The big difficulty is often in selecting good distinguishers (i.e., information that differentiates parties). Generally, it is okay to send differentiating information that one side

does not already have and cannot compute in the clear, because if an attacker tampers

with the information in traffic, the two sides will not be able to agree on a working

key. (Of course, you do need to be prepared to handle such attacks.) Similarly, it is

okay to send a salt. See the sidebar, “Distinguisher Selection,” for a discussion.

The easiest way to get a solid solution that will resist potentially practical attacks is

to use HMAC in counter mode. (Other MACs are not as well suited for this task,

because they tend not to handle variable-length keys.) You can also use this solution

if you want an all-block cipher solution, because you can use a construction to convert a block cipher into a hash function (see Recipes 6.15 and 6.16).

More specifically, key HMAC with the base secret. Then, for every block of output

you need (where the block size is the size of the HMAC output), MAC the distinguishers concatenated with a fixed-size counter at the end. The counter should indicate the number of blocks of output previously processed. The basic idea is to make

sure that each MAC input is unique.

If the desired output length is not a multiple of the MAC output length, simply generate blocks until you have sufficient bytes, then truncate.

The security level of this solution is limited by the minimum of the

number of bits of entropy in the base secret and the output size of the

MAC. For example, if you use a key with 256 bits of entropy, and you

use HMAC-SHA1 to produce a 256-bit derived key, never assume that

you have more than 160 bits of effective security (that is the output

size of HMAC-SHA1).



Algorithmically Generating Symmetric Keys from One Base Secret | 143

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



Distinguisher Selection

The basic idea behind a distinguisher is that it must be unique.

If you want to create a particular derived key, we recommend that you string together

in a predetermined order any interesting information about that key, separating data

items with a unique separation character (i.e., not a character that would be valid in

one of the data items). You can use alternate formats, as long as your data representation is unambiguous, in that each possible distinguisher is generated by a single,

unique set of information.

As an example, let’s say you want to have a different session key that you change once

a day. You could then use the date as a unique distinguisher. If you want to change keys

every time there’s a connection, the date is no longer unique. However, you could use

the date concatenated with the number of times a connection has been established on

that date. The two together constitute a unique value.

There are many potential data items you might want to include in a distinguisher, and

they do not have to be unique to be useful, as long as there is a guarantee that the distinguisher itself is unique. Here is a list of some common data items you could use:

• The encryption algorithm and any parameters for which the derived key will be

used

• The number of times the base key has been used, either overall or in the context

of other interesting data items

• A unique identifier corresponding to an entity in the system, such as a username

or email address

• The IP addresses of communicating parties

• A timestamp, or at least the current date

• The MAC address associated with the network interface being used

• Any other session-specific information

In addition, to prevent against any possible offline precomputation attacks, we recommend you add to your differentiator a random salt of at least 64 bits, which you then

communicate to any other party that needs to derive the same key.



Here is an example implementation of a PRF based on HMAC-SHA1, using the

OpenSSL API for HMAC (discussed in Recipe 6.10):

#include

#include

#include

#include

#include

#define HMAC_OUT_LEN 20 /* SHA1 specific */

void spc_make_derived_key(unsigned char *base, size_t bl, unsigned char *dist,

size_t dl, unsigned char *out, size_t ol) {



144



|



Chapter 4: Symmetric Cryptography Fundamentals

This is the Title of the Book, eMatter Edition

Copyright © 2007 O’Reilly & Associates, Inc. All rights reserved.



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

9 Using Salts, Nonces, and Initialization Vectors

Tải bản đầy đủ ngay(0 tr)

×