10 min read
Pandadrive

Design Document

Baicheng Chen, Jonathan Lin Project Specs

Block Diagram

Block Diagram

Data Structures

Keystore

For each user:

uuid: PKEEncKey	// generate uuid with 'username||pke_ek'
uuid: DSVerifyKey	// generate uuid with 'username||ds_vk'

Datastore

// Encapsulated the structs below for Authenticated Encryption
type AEPayload struct {
	Data []byte			// could be encrypted or not
	Tag	[]byte			// either sig or hmac
	Tag_type string		// either 'sig' or 'hmac'
}

// X Confidentiality
// O Integrity / Authentication:    DSSign() - ds_sk
type Auth struct {
	Username string
	Password_H [16]byte	// hash(argon(password))
	Salt_pwd [16]byte	// argon salt for password
	Salt_mk [16]byte	// argon salt for master key
}

// O Confidentiality:				SymEnc() - sym_ek
// O Integrity / Authentication:    HMACEval() - sym_hk
type UserInfo struct {
	Files_id uuid.UUID	// -> UserFiles
	PKE_ek PKEDecKey
	DS_sk DSSignKey
}

// O Confidentiality:				SymEnc() - sym_ek
// O Integrity / Authentication:    HMACEval() - sym_hk
type UserFiles map[string]uuid.UUID {
	Filename1: User_file_id,	    // -> UserFile
	Filename2: UserFile,
	...
}

// O Confidentiality:				SymEnc() - sym_ek
// O Integrity / Authentication:    HMACEval() - sym_hk
type UserFile struct {
	Owner uuid.UUID			// For verifying integrity of FileCreds
	File_creds_id uuid.UUID	// -> FileCreds
}

// X Confidentiality:
// O Integrity / Authentication:	DSSign() - ds_sk:owner/sender/self
type FileCreds struct {
	File_id	uuid.UUID
	Esk_sym_ek_file [16]bytes
	Esk_sym_hk_file [16]bytes
	Shared_users uuid.UUID			// -> SharedUsers
}

// O Confidentiality:				Hybrid: SymEnc() - sym_ek_filexxx -> PKEEnc() - pke_ek
// O Integrity / Authentication:    HMACEval() - sym_hk_filexxx
type SharedUsers map[string]uuid.UUID {
	Username1: File_creds1,	// -> FileCreds
	Username2: File_creds2,
	...
}


// O Confidentiality:				Hybrid: SymEnc() - sym_ek_filexxx -> PKEEnc() - pke_ek
// O Integrity / Authentication:    HMACEval() - sym_hk_filexxx
type File struct {
	Owner uuid.UUID
	Last_updated int64			// Update on AppendToFile(), StoreFile(), Revoke()
	Content_start uuid.UUID		// -> FileContent
	Content_end uuid.UUID		// -> FileContent
	// Nonce []byte				// random nonce, update after revocation
}


// O Confidentiality:				Hybrid: SymEnc() - sym_ek_filexxx -> PKEEnc() - pke_ek
// O Integrity / Authentication:    HMACEval() - sym_hk_filexxx
type FileContent struct{
	Content []byte
	Next uuid.UUID				// -> FileContent
}

// O Confidentiality:				RSA: PKEEnc() - PKEEncKey:recipient
// O Integrity / Authentication:    DSSign() - ds_sk:sender
type Invitation struct {
	Owner uuid.UUID			// -> To get ds_vk:owner. Needed in cases of revocation (of third party) before acceptance
	File_creds_id uuid.UUID	// -> FileCreds
}

Users and User Authentication

User Authentication: How will you authenticate users?

How does EvanBot create a new user? When they try to log in, how will you check if the username and password are correct? How do you ensure that a user cannot login with an incorrect password?

Either InitUser() (signup) or GetUser() (login) is called, providing username and password.

Auth is stored in Datastore with uuid.FromBytes(Hash([]byte(username))[:16]) as UUID

Signup

  1. Verify that the Auth UUID for username does not exist in Datastore, otherwise abort/error.
  2. Create a new Auth struct
  3. Compute argon hash of password with a random salt, store as Auth.Password_H and salt as Auth.Salt_pwd
  4. Compute second argon hash of password with a different random salt, store hash[:16] locally asmaster_key and salt as Auth.Salt_mk.
  5. Use HashKDF() with master_key to derive sym_ek, sym_hk and store locally.
  6. Create a UserInfo struct
  7. Generate (pke_ek, pke_dk), (ds_sk, ds_vk) with userlib functions and upload public keys to Keystore, store private keys in UserInfo.
  8. Create an empty UserFiles (encrypted, hmac-ed) and upload to Datastore, store its UUID to UserInfo.Files_id
  9. Upload Auth (un-encrypted, signed) and UserInfo (encrypted, hmac-ed) to Datastore.

Login

  1. Use the username derived UUID to retrieve Auth from Datastore. Verify signature with ds_vk from Keystore. Abort/error if integrity compromised or UUID doesn’t exist.
  2. Compute argon hash of password with salt Auth.Salt_pwd and verify it matches Auth.Password_H, abort/error if false.
  3. Login successful, proceed to further operations (omitted here).

File Operations

File Storage and Retrieval: How does a user store and retrieve files?

EvanBot is logged in and stores a file. How does the file get stored in Datastore? What key(s) and UUIDs do you use? How do you access the file at a later time?

Storing files

StoreFile(filename, content) is called.

  1. Check if filename exists in UserFiles.

If exists: follow procedure for accessing files to step 3, then skip to step 5 of storing files . Else continue:

  1. Create new structs UserFile, FileCreds, File, FileContent, and generate random UUIDs for them with uuid.New().
  2. UserFile: Set Owner to Auth UUID of self, File_creds_id to correct UUID.
  3. FileCreds: Set File_id to correct UUID. Generate random symmetric keys sym_ek_file, sym_hk_file and encrypt them with pke_ek, then store to Esk_sym_ek_file, Esk_sym_hk_file.
  4. FileContent: Set Content to file’s content, Next to randomly generated UUID.
  5. File: Set Owner to Auth UUID of owner, Last_updated to current Unix time in milliseconds, Content_start to UUID of FileContent and Content_end to FileContent.Next
  6. Set UserFiles[filename] to UUID of UserFile
  7. Hybrid encrypt File, FileContent with (sym_ek_file, pke_ek) and hmac with sym_hk_file. Upload the two structs
  8. Encrypt UserFile with sym_ek and hmac with sym_hk. Upload the struct.
  9. Sign FileCreds with ds_sk and upload the struct.

Accessing files

  1. Retrieve UserFile from its UUID in UserFiles[filename]. Abort/Error if not found.
  2. Retrieve FileCreds and verify its signature (has to match ds_sk of owner/sender/self). Abort/error if integrity compromised.
  3. Decrypt Esk_sym_ek_file, Esk_sym_hk_file with pke_dk to obtain sym_ek_file, sym_hk_file
  4. Retrive File, verify hmac with sym_hk_file and decrypt with sym_ek_file
  5. Retrive Content_start and verify hmac + hybrid decrypt in the same way, and repeat for all Next FileContents to build the whole file.

Efficient Append: What is the total bandwidth used in a call to append?

List out every piece of data that you need to upload (DatastoreSet) or download (DatastoreGet) from Datastore in a call to append, and the size of each piece of data. Is the total a constant, or does it scale?

A call of User.AppendToFile(filename string, content []byte) entails:

  1. Download UserFile, FileCreds, File (steps for accessing files)
  2. Create new FileContent
  3. Write to FileContent.Content and store new random UUID FileContent.Next
  4. Update File.Last_updated and set File.Content_end to the new FileContent.Next
  5. Upload FileContent to File.Content_end and upload File

Examining the UserFile, FileCreds, File, FileContent sructs, the bandwidth only scales with content size and has constant overhead.

How will you ensure that multiple User objects for the same user always see the latest changes reflected?

EvanBot logs in on their laptop and phone, creating two User objects. If EvanBot makes a change on their laptop (e.g. storing a file), how do you ensure that EvanBot will see the change reflected on their phone?

Store a timestamp Last_retrieved (at local) of the time that EvaBot retrieved the file. Then during any read/write operations, compare it to File.Last_updated on Datastore. Re-load file when Last_retrieved is older.

Sharing and Revocation

File Sharing: What gets created on CreateInvitation, and what changes on AcceptInvitation?

CodaBot (not the file owner) wants to share the file with PintoBot. What is the sharing process like when a non-owner shares? (Same questions as above; your answers might be the same or different depending on your design.)

CreateInvitation

CreateInvitation(filename, recipientUsername):

  1. Create a new Invitation and new FileCreds with random UUIDs.
  2. Encrypt sym_ek_file and sym_hk_file with the recipient’s pke_ek, store in FileCreds
  3. Store the File_id pointing to File
  4. Store the new FileCreds and Owner UUID in Invitation
  5. Encrypt Invitation with the recipient’s pke_ek and sign both Invitation, FileCreds with sender’s ds_sk (UUID derived from recipientUsername). Upload the two structs.
  6. In the sender’s own UserFiles[filename]->UserFile->FileCreds.Shared_users (create if nil), add the UUID of the new FileCreds to SharedUsers[recipient name].
  7. Encrypt and hmac SharedUsers the same way as when storing files then upload.

AcceptInvitation

AcceptInvitation(senderUsername, invitationPtr, filename):

  1. Retrieve the Invitation at invitationPtr, verify signature with ds_sk of sender (UUID derived from senderUsername). Abort/error if integrity compromised.
  2. Decrypt with recipient’s own pke_dk.
  3. Create a new UserFile with random UUID, populate with contents of Invitation and create corresponding entry in UserFiles

Sharing proccess of non-owner

Exactly the same as how the owner does the CreateInvitation process.

File Revocation: What values need to be updated when revoking?

How do you ensure that a Revoked User Adversary cannot read or modify the file without being detected, even if they can directly access Datastore and remember values computed earlier? How do you ensure that a Revoked User Adversary cannot learn about when future updates are happening?

A file owner calling RevokeAccess(filename string, recipientUsername string) entails:

  1. Access the file
  2. Generate new sym_ek_file, sym_hk_file keys
  3. Re-encrypt the file in the same way as “storing files”
  4. Move File to a new random UUID and delete the old location. Also update File.Last_updated.
  5. Traverse FileCreds->Shared_users to find the recipientUsername. Traverse from that node recursively to delete the node and all children FileCreds nodes from Datastore.
  6. Update all the other remaining FileCreds nodes (including owner’s) with the new keys (encrypted with respective pke_ek) and new UUID of File
A tree with nodes ABCDEFG.

For example, when UserA calls RevokeAccess(file, UserC), they will traverse the tree to delete FileCreds, SharedUsers of users C, E, F and update FileCreds for users B, D, G

Helper Functions

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.

Hybrid Encryption

func HybridEnc(ek userlib.PKEEncKey, key []byte, iv []byte, plaintext []byte) (esk []byte, ciphertext []byte, err error)
  1. Encrypt plaintext using the symmetric key
  2. Encrypt the symmetric key using the provided public key
  3. Return the encrypted symmetric key and the ciphertext
func HybridDec(dk userlib.PKEDecKey, esk []byte, ciphertext []byte) (plaintext []byte, err error)
  1. Decrypt the symmetric key using the provided private key
  2. Decrypt the ciphertext using the decrypted symmetric key
  3. Return the decrypted plaintext

Encrypt-then-MAC (Authenticated Encryption)

func EncThenMac(plaintext []byte, ek []byte, hk byte[]) (payload []byte, err error)
  1. Encrypt the plaintext with key ek
  2. Compute HMACEval() on the ciphertext with key hk
  3. Return hmac joined with ciphertext as payload