Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rfc] Application Security Architecture #65

Open
pkarw opened this issue Jul 31, 2024 · 1 comment
Open

[rfc] Application Security Architecture #65

pkarw opened this issue Jul 31, 2024 · 1 comment
Assignees
Labels
prio-critical RFC This is request for comments

Comments

@pkarw
Copy link
Contributor

pkarw commented Jul 31, 2024

Terms:

  • Database ID - unique name of the current database used by the application; this could be for example Personal ID / Social Security number / PESEL (PL) or any other unique value - never sent plain to the server (only hash)
  • User Key - encryption key, unique, not stored on the server - used to access users's Master Key which is used to encrypt/decrypt data; data is only encrypted and decrypted Client Side thus we're in Zero Trust security
  • Master Key - encrypted using AES-GCM (encryption key = User Key)
  • Sharing Key - generated by user 6 digits (or more) key used to decrypt Master Key - but only for sharing purposes, subject to expiration date (for example valid just for 30min or one day)

We're using double leveled structure (User Key / Sharing Key) -> (Master Key) for letting the Users to manage/change Sharing and User Keys without the need to re-encrypt whole database.

Database Structure:

  • Table: keys - used for storing User Keys and Sharing Keys - therefore for API access authorization + for providing the client with encrypted Master Key for being used for data encryption and decryption:
    • databaseIdHash: Unique SHA-256 hash of the Database ID.
    • keyLocatorHash: Unique hash of the User Key or Sharing Key + Database Id using SHA256, unique within same Database ID. Hash is generated client side
    • keyHashParams: parameters - including salt - used for hashing keyHash; provided by client with generated hash; it's a JSON of:
{  "salt": "salt",
    "time": 1, // the number of iterations
    "mem": 1024, // used memory, in KiB
    "hashLen": 24, // desired hash length
    "parallelism": 1, // desired parallelism (it won't be computed in parallel, however)
}
  • keyHash: Unique hash of the User Key or Sharing Key using Argon2id, unique within same Database ID. Hash is generated client side
  • encryptedMasterKey: The master key encrypted using AES-GCM, secured with a User Key (which the server never receives in plain text). Master Key MUST be generated client side, encrypted and sent in encrypted form to the server.
  • expiryDate: Other fields may include expiration date and additional data.

Each databaseIdHash is associated with a at least single User Key and can have multiple Sharing Keys. Each Sharing Key record holds an encrypted copy of the Master Key.

Zero Trust
Because we never store plain User Keys, Shared Keys neither Master Keys - we're in Zero Trust security. The hashes (being sent to server) and the data encryption/decryption (done only on client) are only possible with given and stored inside browser's memory User Key and Master Keys.

UC01: Creating a New User Key or Database:

User provides us with:

  • Database Id - must be unique for the entire app as it's translated to database file name,
  • User Key / Password - automatically generated, subject to user change key.

Endpoint: /db/create

Data sent to server:

  • databaseIdHash: sha256(Database Id)
  • keyLocatorHash: sha256(User Key + Database Id + static salt)
  • keyHash: argon2Id(User Key)
  • encryptedMasterKey - encrypted by User Key key used to decrypt and encrypt users data
  • keyHashParams: hash params used for arg2id in JSON format:
{  "salt": "salt",
    "time": 1, // the number of iterations
    "mem": 1024, // used memory, in KiB
    "hashLen": 24, // desired hash length
    "parallelism": 1, // desired parallelism (it won't be computed in parallel, however)
}
  • masterDataKey - AES encrypted randomly generated key used for encrypting all subsequent data - key to encrypt this master key is always plain User Key or Sharing key which is never sent back to the server.

In case this is User Key - the first key in the database we are also generating and encrypting with AES-GCM the masterDataKey. If this is another key request - we're just sending the existing key along.

Hashes are calculated Client Side. No plain data sent to server.

For every API request we will set the Authorization header contains base64(JWT Access Token). The server verifies this as described below later in this text.

Server side:

  • Server creates new keys record storing:
  • databaseIdHash as sent from the client
  • keyLocatorHash - sha256 record locator with static salt,
  • keyHash - argon2Id hash - calculated client side and here stored for further usage,
  • keyHashParams - argon2Id params - used for further hashing.

UC02: Log In

  1. The user inputs their Database Id.
  2. The user inputs their User Key or Sharing Key.
  3. Application is requesting /db/authorize-challenge with posting:
  • databaseIdHash: sha256(Database Id)
  • keyLocatorHash: sha256(User Key + Database Id + static salt) - this is just the record locator
  1. Server sents back keyHashParams associated with the databasIdHash and keyLocatorHash' used as record locators inside keys` table
  2. Client calculates argon2id hash for given keyHashParams and User Key and sends it to the server as keyHash with a request to /db/authorize
  3. Server checks if argon hash associated with the key located by keylocatorHash and databaseIdHash matches keyHash.
  4. Note: After succesfull login, server responds with JWT Access Token and JWT Refresh Token. These details are stored in the browser's memory and sent with each API request.
  5. In case of succesfull login, The browser receives an AES-encrypted Master Key for the data, which is decrypted only in the browser using the User Key or Sharing Key stored in memory browser and unavailable to the server.
  6. Data is encrypted using AES-GCM with this master key.

Authorization algorithm (on the server):

  1. Get the keys records (one or many) where databaseIdHash = + databaseIdHash + and keyLocatorHash = + keyLocatorHash (sent from client)
  2. Iterate over the keys assigned to specific databaseHash and check if argon2id.check(keyHash, serverStoredKey.keyHash) are matched.
  3. If so, send the encryptedMasterKey + JWT token to the client.

UC03: API Request Handling by the Server

  1. Server verifies JWT Access token sent in the x-access-token header.
  2. The server searches for the key in the database using the keyHash (hashed Database ID).
  3. keyHash and databaseIdHash are stored within JWT Token described above.

Alternatively:

  1. Server retrieves all keys for the tenant (there can be multiple if shared, or one if not). It iterates through and checks argon2id (of the submitted client key hash). If matched, the user is authorized

Client-Side Encryption and Decryption:
All data encryption and decryption are handled on the client side, ensuring data security.

The server cannot decrypt client data as it never receives the user's key—only the SHA-256 hash of the key, which is necessary to decrypt the master key used for data encryption.

@pkarw pkarw added the RFC This is request for comments label Jul 31, 2024
@pkarw pkarw self-assigned this Jul 31, 2024
@pkarw
Copy link
Contributor Author

pkarw commented Aug 3, 2024

Implemented with #66

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
prio-critical RFC This is request for comments
Projects
None yet
Development

No branches or pull requests

1 participant