Skip to main content Link Search Menu Expand Document (external link)

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.

Design Question: 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.

Keystore

userlib.KeystoreSet(name string, value PKEEncKey/DSVerifyKey) (err error)

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(name string) (value PKEEncKey/DSVerifyKey, ok bool)

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(name UUID, value []byte)

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(name UUID) (value []byte, ok bool)

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(key UUID)

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() (uuid.UUID)

Returns a randomly generated UUID.

Note: 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(b []byte) (uuid UUID, err error)

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.

Note: 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(v interface{}) (bytes []byte, err error)

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(bytes))
// {"Username":"alice","Password":"password"}
json.Unmarshal(v []byte, obj interface{}) (err)

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

RandomBytes(bytes int) (data []byte)

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.

Cryptographic Hash

Hash(data []byte) (sum []byte)

Takes in arbitrary-length data, and outputs sum, a 64-byte SHA-512 hash of the data.

Note: you should use HMACEqual to determine hash equality. This function runs in constant time and avoids timing side-channel attacks.

Symmetric-Key Encryption

SymEnc(key []byte, iv []byte, plaintext []byte) (ciphertext []byte)

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.

SymDec(key []byte, ciphertext []byte) (plaintext []byte)

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.

HMAC

HMACEval(key []byte, msg []byte) (sum []byte, err error)

Takes in an arbitrary-length msg, and a 16-byte key. Computes a 64-byte HMAC-SHA-512 on the message.

HMACEqual(a []byte, b []byte) (equal bool)

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 Encryption

PKEEncKey: A data type for RSA public (encryption) keys.

PKEDecKey: A data type for RSA private (decryption) keys.

PKEKeyGen() (PKEEncKey, PKEDecKey, err error)

Generates a 256-byte RSA key pair for public-key encryption.

PKEEnc(ek PKEEncKey, plaintext []byte) (ciphertext []byte, err error)

Uses the RSA public key ek to encrypt the plaintext, using RSA-OAEP.

PKEDec(dk PKEDecKey, ciphertext []byte) (plaintext []byte, err error)

Use the RSA private key dk to decrypt the ciphertext.

Note: 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. Then, 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. Then, use the symmetric key to decrypt the data.

Digital Signatures

DSSignKey: A data type for RSA private (signing) keys.

DSVerifyKey: A data type for RSA public (verification) keys.

DSKeyGen() (DSSignKey, DSVerifyKey, err error)

Generates an RSA key pair for digital signatures.

DSSign(sk DSSignKey, msg []byte) (sig []byte, err error)

Given an RSA private (signing) key sk and a msg, outputs a 256-byte RSA signature sig.

DSVerify(vk DSVerifyKey, msg []byte, sig []byte) (err error)

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.

Password-Based Key Derivation Function

Argon2Key is a slow hash function, designed specifically for hashing passwords.

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.

You can assume that the user’s chosen password has sufficient entropy for the PBKDF output to 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.

Argon2Key(password []byte, salt []byte, keyLen uint32) (result []byte)

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.

Hash-Based Key Derivation Function

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.

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.

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.

HashKDF(sourceKey []byte, purpose []byte) (derivedKey []byte, err error)

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.

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 }