Library Functions
In this section, we provide documentation for some cryptographic functions and some utility helper functions that you can use at any time in your design. These functions have already been implemented for you in the project2-userlib library, which will be imported for you in the starter code.
Please carefully read through the provided functions while coming up with your design so that you are aware of what is possible to actually implement in code.
You cannot import any libraries besides what we’ve already imported in the starter code. You should not need any external libraries for this project.
You should not write your own cryptographic functions for this project. For example, you shouldn’t write code to implement AES-CTR yourself. Instead, you should call the existing SymEnc
function that we’ve provided.
As discussed in class, you should avoid any unsafe cryptographic design patterns, such as reusing the same keys in different algorithms (see the tips section for more details), or using MAC-then-encrypt.
Helper functions: As you come up with a design, think about any helper functions you might write in addition to the cryptographic functions included here.
Having helper functions can simplify your code. Consider authenticated encryption, hybrid encryption, etc.
Types
As you read through the functions, it may be useful to take note of the following datatypes:
PKEEncKey
: An RSA public key. Recall that public keys are used to encrypt data.PKEDecKey
: An RSA private key. Recall that private keys are used to decrypt data.DSSignKey
: An RSA signing key. Recall that signing keys are used to create digital signatures.DSVerifyKey
: An RSA verification key. Recall that verification keys are used to verify digital signatures.uuid.UUID
: A UUID created throughuuid.New
oruuid.FromBytes
, used as a key for the Datastore.[]bytes
: An array of arbitrary bytes. You must turn other types into an array of bytes usingjson.Marshal
.
Keystore
userlib.KeystoreSet
Method Signature:
userlib.KeystoreSet(name string, value PKEEncKey|DSVerifyKey) (err error)
Description:
Stores a name
and value
as a name-value pair into Keystore. The name can be any unique string, and the value must be a public key. You cannot store any data that is not a public key in Keystore.
Keystore is immutable: A name-value pair cannot be modified or deleted after being stored in Keystore. Any attempt to modify an existing name-value pair will return an error.
userlib.KeystoreGet
Method Signature:
userlib.KeystoreGet(name string) (value PKEEncKey|DSVerifyKey, ok bool)
Description:
Looks up the provided name
and returns the corresponding value
.
If a corresponding value exists, then ok
will be true; otherwise, ok
will be false.
Datastore
userlib.DatastoreSet
Method Signature:
userlib.DatastoreSet(name uuid.UUID, value []byte)
Description:
Stores name
and value
as a name-value pair into Datastore.
Datastore is mutable: If name
already maps to an existing name-value pair, then the existing value will be overwritten with the provided value
.
userlib.DatastoreGet
Method Signature:
userlib.DatastoreGet(name uuid.UUID) (value []byte, ok bool)
Description:
Looks up the provided name
and returns the corresponding value
.
If a corresponding value exists, then ok
will be true; otherwise, ok
will be false.
userlib.DatastoreDelete
Method Signature:
userlib.DatastoreDelete(key uuid.UUID)
Description:
Looks up the provided name
and deletes the corresponding value, if it exists.
UUID
Recall that in the name-value pairs of Datastore, the name should be a UUID. UUID stands for Universal Unique Identifier, and is a unique 16-byte (128-bit) value.
There are two ways to create UUIDs. You can randomly generate a new UUID from scratch. Alternatively, you can take an existing 16-byte string, and deterministically cast it into a UUID.
The uuid
library also provides uuid.Nil
, a UUID consisting of all zeros to represent a nil value.
uuid.New
Method Signature:
uuid.New() (uuid.UUID)
Description:
Returns a randomly generated UUID.
If you’re concerned about two randomly-generated UUIDs being the same, think about the probability that two randomly-generated 128-bit values are identical. In this project, you don’t have to worry about events that are astronomically unlikely to occur.
uuid.FromBytes
Method Signature:
uuid.FromBytes(b []byte) (uuid uuid.UUID, err error)
Description:
Creates a new UUID by copying the 16 bytes in b
into a new UUID.
Returns an error if the byte slice b
does not have a length of 16.
This function does not apply any additional security to the inputted byte slice. You can think of this function as casting a 16-byte value into a UUID. Anybody who reads the UUID will be able to determine what 16-byte value you used to generate the UUID, so you should not pass sensitive information into this function.
JSON Marshal and Unmarshal
Recall that in the name-value pairs of Datastore, the value should be a byte array.
If you want to store other types of data (e.g. structs) in Datastore, you will need to convert that data into a byte array before storing it. Then, you will need to convert the byte array back into the original data structure when retrieving the data.
We’ve provided the json.Marshal
serialization function, which takes any arbitrary data and converts it into a byte array.
We’ve also provided the json.Unmarshal
deserialization function, which takes a byte array outputted by json.Marshal
, and converts it back into the original data.
json.Marshal
Method Signature:
json.Marshal(v interface{}) (bytes []byte, err error)
Description:
Converts an arbitrary Go value, v
, into a byte slice containing the JSON representation of the struct.
If the value is a struct, only fields that start with a capital letter are converted. Fields starting with a lowercase letter are not marshaled into the output.
This function will automatically follow Go memory pointers (including nested Go memory pointers) when marshalling.
// Serialize a User struct into JSON. type User struct { Username string Password string lostdata int } alice := &User{ "alice", "password", 42, } aliceBytes, err := json.Marshal(alice) userlib.DebugMsg("%s\n", string(aliceBytes)) // {"Username":"alice","Password":"password"}
json.Unmarshal
Method Signature:
json.Unmarshal(v []byte, obj interface{}) (err)
Description:
Converts a byte slice v
, generated by json.Marshal, back into a Go struct. Assigns obj
to the converted Go struct.
Only struct fields that start with a capital letter will have their values restored. Struct fields that start with a lowercase letter will be initialized to their default value.
This function automatically generates nested Go memory pointers where needed to generate a valid struct.
This function will return an error if there is a type mismatch between the JSON and the struct (e.g. storing a string into a number field in a struct).
// Serialize a User struct into JSON. // The lostdata field will NOT be included in the byte array output. type User struct { Username string Password string lostdata int } aliceBytes := []byte("{\"Username\":\"alice\",\"Password\":\"password\"}") var alice User err = json.Unmarshal(aliceBytes, &alice) if err != nil { return } userlib.DebugMsg("%v\n", alice) // {alice password 0}
Random Byte Generator
userlib.RandomByte
Method Signature:
userlib.RandomBytes(bytes int) (data []byte)
Description:
Given a length bytes
, return that number of randomly generated bytes.
The random bytes returned could be used as an IV, symmetric key, or anything else you’d like.
You don’t need to worry about the underlying implementation (e.g. you don’t have to think about reseeding any PRNG). You can assume the returned bytes are indistinguishable from truly random bytes.
Hash Functions
userlib.Hash
Method Signature:
userlib.Hash(data []byte) (sum []byte)
Description:
Takes in arbitrary-length data
, and outputs sum
, a 64-byte SHA-512 hash of the data.
You should use HMACEqual to determine hash equality. This function runs in constant time and avoids timing side-channel attacks.
userlib.HashKDF
Method Signature:
userlib.HashKDF(sourceKey []byte, purpose []byte) (derivedKey []byte, err error)
Description:
HashKDF stands for Hash-Based Key Derivation Function. It hashes together a 16-byte sourceKey
and some arbitrary-length byte array purpose
to deterministically derive a new 64-byte derivedKey
.
If you don’t need all 64 bytes of the output, you can slice to obtain a key of the desired length.
You can use the HashKDF to deterministically derive multiple keys from a single root key. This can simplify your key management schemes.
HashKDF is a fast hash function, similar to HMAC, that essentially hashes the source key and the purpose together. Changing either the source key, or the purpose, or both, will cause the output of HashKDF to be unpredictably different.
If the source key is insecure (e.g. an attacker knows its value), and the purpose is insecure (e.g. it’s a hard-coded string and the attacker has a copy of your code), then the derived keys outputted by HashKDF will also be insecure.
One way you can use HashKDF is by calling it multiple times with the same source key but different, hard-coded purposes every time. This will generate multiple keys, one per call to HashKDF. Anybody who knows the source key and the purposes can re-generate the keys by calling HashKDF again (without needing to store the derived keys). Anybody who doesn’t know the source key will be unable to generate the keys.
Here’s a code snippet showing how you could use HashKDF to take one source key and derive two keys, one for encryption and one for MACing.
sourceKey := userlib.RandomBytes(16) encKey, err := userlib.HashKDF(sourceKey, []byte("encryption")) if err != nil { return } macKey, err := userlib.HashKDF(sourceKey, []byte("mac")) if err != nil { return }
userlib.Argon2Key
Method Signature:
userlib.Argon2Key(password []byte, salt []byte, keyLen uint32) (result []byte)
Description:
Argon2Key is a slow hash function, designed specifically for hashing passwords. It applies a slow hash to the given password
and salt
. The outputted hash is keyLen
bytes long, and can be used as a symmetric key.
Argon2Key is called a Password-Based Key Derivation Function (PBKDF) because the output (i.e. the hashed password) can be used as a symmetric key. An attacker cannot brute-force passwords to learn the key because the hash function is too slow. Also, the hash function makes the hashed password look unpredictably random, so it can be used as a symmetric key.
The salt
argument is used to ensure that two users with the same password don’t have the same password hash. If you choose to use the hash as a key, then the salt also ensures that the two users don’t use the same key.
For this project, you may assume that the user’s chosen password has sufficient entropy for the PBKDF output to be used as a symmetric key.
Symmetric-Key Cryptography
userlib.SymEnc
Method Signature:
userlib.SymEnc(key []byte, iv []byte, plaintext []byte) (ciphertext []byte)
Description:
Encrypts the plaintext
using AES-CTR mode with the provided 16-byte key
and 16-byte iv
.
Returns the ciphertext
, which will contain the IV (you do not need to store the IV separately).
This function is capable of encrypting variable-length plaintext, regardless of size. You do not need to pad your plaintext to any specific block size.
userlib.SymDec
Method Signature:
userlib.SymDec(key []byte, ciphertext []byte) (plaintext []byte)
Description:
Decrypts the ciphertext
using the 16-byte key
.
The IV should be included in the ciphertext (see SymEnc
).
If the provided ciphertext
is less than the length of one cipher block, then SymDec
will panic (remember, your code should always return errors, and not panic).
Notice that the SymDec method does not return an error. In other words, if some ciphertext has been mutated, SymDec will return non-useful plaintext (e.g. garbage), since AES-CTR mode does not provide integrity.
userlib.HMACEval
Method Signature:
userlib.HMACEval(key []byte, msg []byte) (sum []byte, err error)
Description:
Takes in an arbitrary-length msg
, and a 16-byte key. Computes a 64-byte HMAC-SHA-512 on the message.
userlib.HMACEqual
Method Signature:
userlib.HMACEqual(a []byte, b []byte) (equal bool)
Description:
Compare whether two HMACs (or hashes) a
and b
are the same, in constant time.
If a
and b
are the same HMAC/hash, then equals
will be true; otherwise, equals
will be false.
Public-Key Cryptography
userlib.PKEKeyGen
Method Signature:
PKEKeyGen() (PKEEncKey, PKEDecKey, err error)
Description:
Generates a 256-byte RSA key pair for public-key encryption.
userlib.PKEEnc
Method Signature:
userlib.PKEEnc(ek PKEEncKey, plaintext []byte) (ciphertext []byte, err error)
Description:
Uses the RSA public key ek
to encrypt the plaintext
, using RSA-OAEP.
userlib.PKEDec
Method Signature:
PKEDec(dk PKEDecKey, ciphertext []byte) (plaintext []byte, err error)
Description:
Use the RSA private key dk
to decrypt the ciphertext
.
RSA encryption does not support very long plaintext. If you need to use a public key to encrypt long plaintext, consider writing a helper function that implements hybrid encryption.
Recall the hybrid encryption process:
- Use the given public key to encrypt a random symmetric key.
- Use the symmetric key to encrypt the actual data.
- Return the symmetric key (encrypted with the public key) and the data (encrypted with the symmetric key).
Recall the decryption process for hybrid encryption schemes:
- Use the given private key to decrypt the symmetric key.
- Use the symmetric key to decrypt the data.
userlib.DSKeyGen
Method Signature:
userlib.DSKeyGen() (DSSignKey, DSVerifyKey, err error)
Description:
Generates an RSA key pair for digital signatures.
userlib.DSSign
Method Signature:
userlib.DSSign(sk DSSignKey, msg []byte) (sig []byte, err error)
Description:
Given an RSA private (signing) key sk
and a msg
, outputs a 256-byte RSA signature sig
.
userlib.DSVerify
Method Signature:
userlib.DSVerify(vk DSVerifyKey, msg []byte, sig []byte) (err error)
Description:
Uses the RSA public (verification) key vk
to verify that the signature sig
on the message msg
is valid. If the signature is valid, err
is nil
; otherwise, err
is not nil
.