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
andvalue
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 correspondingvalue
.If a corresponding value exists, then
ok
will be true; otherwise,ok
will be false.
Datastore
userlib.DatastoreSet(name UUID, value []byte)
Stores
name
andvalue
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 providedvalue
.
userlib.DatastoreGet(name UUID) (value []byte, ok bool)
Looks up the provided
name
and returns the correspondingvalue
.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(aliceBytes)) // {"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. Assignsobj
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 outputssum
, 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-bytekey
and 16-byteiv
.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-bytekey
.The IV should be included in the ciphertext (see
SymEnc
).If the provided
ciphertext
is less than the length of one cipher block, thenSymDec
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
andb
are the same, in constant time.If
a
andb
are the same HMAC/hash, thenequals
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 theplaintext
, using RSA-OAEP.
PKEDec(dk PKEDecKey, ciphertext []byte) (plaintext []byte, err error)
Use the RSA private key
dk
to decrypt theciphertext
.
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 amsg
, outputs a 256-byte RSA signaturesig
.
DSVerify(vk DSVerifyKey, msg []byte, sig []byte) (err error)
Uses the RSA public (verification) key
vk
to verify that the signaturesig
on the messagemsg
is valid. If the signature is valid,err
isnil
; otherwise,err
is notnil
.
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
andsalt
. The outputted hash iskeyLen
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 arraypurpose
to deterministically derive a new 64-bytederivedKey
.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 }