Service Anywhere (SAW) is a ticketing system, such that each ticket display includes a form with multiple fields like description, priority, etc. Administrators will be able to define encryption domains (domain for HR, domain for finance, etc.) and associate users to these domains. Upon association, they also create a passcode to these users. Then, the administrators will be able to define certain text fields as accessible, for both write and read actions, only for a certain domain. Since the data in these fields is sensitive, it should be saved to the server encrypted and only the client side, given the user password, can decrypt the data.
##High level design
We will be using client-side encryption, so no sensitive decrypted data is ever stored on the server side. That includes passwords and encryption keys as well as the data itself. Each domain will have it's own unique symmetric encryption key that will be replicated to each domain user's accounts and encrypted with the user's own password. Granting a user access to specific domain involves a two step operation when administrator user copies domain key to the user's account encrypting it with a temporary passcode that is being passed to the user and then the user confirms the join of the domain by entering her password and received passcode.
Suggested encryption algorithm is AES using the CryptoJS package. Suggested user authentication strategy is by bearer token (RFC-6750) using JWT
Alternative approach Store the domain keys centrally encrypted with domain passcode and have a user enter the passcode when she encounters an encrypted field in the ticket form.
[{
name: "title",
title: "Title",
type: "text",
domain: null
},{
name: "priority",
title: "Priority",
type: "select",
domain: null
},{
name: "description",
title: "Ticket Description",
type: "textarea",
domain: null
},{
name: "sensitive1",
title: "Sensitive finance info",
type: "text",
domain: "finance"
},{
name: "sensitive2",
title: "Sensitive HR info",
type: "text",
domain: "HR"
}];
###ITicket
{
id: "248d40a2-a31d-11e5-b946-33ec746d333f",
title: "Sample ticket",
priority: "Blocker",
description: "Long description text",
// Encrypted with finance domain key:
sensitive1: "U2FsdGVkX18XVlhMqx8ZvJE+HkbE5SXLL60kgPiyf9hky/tdXzHd/6iVVtWxy5cr"
// Encrypted with HR domain key:
sensitive2: "U2FsdGVkX1/b+NF5foQMyYQByPLHo3ITga7wm9HrovE9jDPTr5T/K2OYRnpWU9bD"
}
{
name: "finance",
// Encrypted with admin domain key:
key: "U2FsdGVkX1/u9mvCKjatOeWMdld81YDrRXR9vbEN1Bj5CV6TI6u7A94TebolvFUL"
}
{
username: "vpoupkine",
// Password hash with username salt:
// CryptoJS.PBKDF2(password, user.username, { keySize: 256/32 }).toString();
password: "70a38b7483a578292a525a84b0a7e85b0583b92a8b77de0988ad006a23e01f52",
// JWT token. Not stored in DB. Filled upon user login.
token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjU1YzVjYjJhNDA1NTYwYj"
domains: [{
name: "finance",
// Encrypted with user password:
key: "U2FsdGVkX1/u9mvCKjatOeWMdld81YDrRXR9vbEN1Bj5CV6TI6u7A94TebolvFUL",
passcodeRequired: false
},{
name: "hr",
// Encrypted with user password:
key: "U2FsdGVkX1/u9mvCKjatOeWMdld81YDrRXR9vbEN1Bj5CV6TI6u7A94TebolvFUL",
passcodeRequired: true
}]
}
When the server DB does not contain any users and domains, new admin user will be created upon the first system access. The following steps will be executed on the client in case no users/domains were defined yet:
- Generate admin domain encryption key
- Obtain first admin username and password from user
- Generate password hash for the first admin user
- Encrypt the
admin
domain key with first admin user password hash as encryption key - Create
admin
domain with the key set to result of the previous operation - Submit first admin user credentials with domain set to "admin" and the
admin
domain key to the server. - The server should validate that no users/domains exist to accept the first user creation request.
- Obtain new domain name
- Generate new domain key
- Encrypt the new domain key with
admin
domain key - Store new domain in backend
- Generate temporary passcode
- Decrypt
admin
domain key with admin user password - Decrypt target domain with the
admin
domain key - Encrypt target domain with the temporary passcode
- Store assigned domain to user record
- Send temporary passcode notification to the user
- Receive a temporary passcode notification from admin
- Decrypt assigned domain key with the passcode
- Encrypt assigned domain key with user's password
- Submit password-encrypted domain key back to the server
- Throw away the passcode :)
- Submit username and password
- Create password hash and verify against backend
- Obtain user bearer key and user info from the server
- Decrypt user assigned domain keys with plain text password
- Cache bearer key and decrypted domain keys in sessionStorage or cookie with httpOnly option or even just cache in an angular service but that would require entering password upon any page refresh. Need to evaluate the real risks here.
- Clean the bearer key and domain keys from cache
- Invalidate views
- Submit new password
- Encrypt cached domain keys with new password
- Create new password hash
- Submit new password hash and newly encrypted domain key to the server
-
Request ticket fields list from server (should cache that)
-
Server Side filter ticket fields by user domain - return only public fields and fields accessible to given domain
-
Request ticket from server by id
-
Server Side return only authorized fields values
-
Decrypt encrypted fields with domain key:
ticketFields.forEach(function(field){ // Do not check the domain value assuming // server already has filtered out unauthorized domains if (field.domain) { ticket[field.name] = AuthenticationService.decrypt(ticket[field.name], field.domain); } });
-
Render ticket form
Alternative approach
- Upon user login obtain only the bearer key
- Render ticket form using specially crafted directive to hide contents of encrypted fields
- Upon user encountering an encrypted field ask the user for field's domain passcode and show decrypted field contents
- Encrypt non-public ticket fields with appropriate domain keys
- Submit ticket fields to the server
- Caching decrypted domain key is a potential vulnerability. Not caching the key will lead to poor UX as the user will be required to enter her password on every occasion.
- Should a domain key leak to unwanted party, besides re-encrypting all domain-encrypted values we will need to reset all user passwords or issue temporary passcodes to every user in domain in order to change the domain key system-wide.
POST /user/#
Check user credentials and return user data including assigned domains and their keys
Name | Type | Description |
---|---|---|
username | string | Username |
password | string | Hashed user password |
return |
IUser |
User data excluding password, including JWT token |
Provides ITicketFields by ticket id, ticket category or some other properties identifying a ticket or a group of tickets.
GET /ticket/:id/meta
Provides tickets list and ticket details (ITicket
)
GET /ticket/:id
AuthenticationService.login(username, password) : Promise(IUser)
Authenticate user by username/password pair and cache credentials and domain encryption key
Name | Type | Description |
---|---|---|
username | string | Username |
password | string | Plain text user password |
return |
Promise | Resolved to IUser sans-password |
AuthenticationService.logout()
Clear user data and credentials/keys from the cache
AuthenticationService.encrypt(data, domain)
Encrypt the provided plain text data with specified domain key.
If the domain key is not available in cache assume user session expired and re-login the user:
throw notLoggedIn
exception, broadcast login
event, pop up a login dialog, etc.
Name | Type | Description |
---|---|---|
data | string | Plain text data |
domain | string | Domain to encrypt the data for |
return |
string | Data encrypted with user's domain key |
AuthenticationService.decrypt(data, domain)
Decrypt the provided plain text data with specified domain key.
If the domain key is not available in cache assume user session expired and re-login the user:
throw notLoggedIn
exception, broadcast login
event, pop up a login dialog, etc.
Name | Type | Description |
---|---|---|
data | string | Data encrypted with user's domain key |
domain | string | Domain to decrypt the data for |
return |
string | Decrypted plain text data |