I am working on a sync service for my desktop app. The app tracks (with the user's consent) some computer usage data (comparable in terms of sensitivity to their browser history), so I'd like for this data to protected from accidental data leaks and hacks as much as possible. I do not expect an attacker specifically targeting this system, though, given that it is still fairly niche.
Below is an explanation of my current plan for data security.
- Local events are consolidated in an "operation log". Entries in this log are compressed using Zstandard.
- These (compressed) events are then uploaded via gRPC to my service (written in the Swift programming language) running in a containerized environment on Google Kubernetes engine.
- Upon receipt, the service encrypts the payload of each log entry using AES256-CBC with a per-user data encryption key (DEK). The initialization vector is generated as the first 16 bytes of the SHA1 hash of the following elements:
- The current server timestamp.
- A random 64-bit ID for the entry provided by the client.
- An SHA1 hash of the unencrypted payload data. The service stores the plaintext metadata (SHA1 hash of the unencrypted data, server timestamp, random client ID) and the encrypted payload in a PostgreSQL database.
- When another client (authenticated with the same user, see below) requests the operation log, each entry is fetched from the database and decrypted using the same IV derivation method as above. The decrypted payload is then transferred to the client. The client verifies the integrity of the payload by hashing it using SHA1 and comparing the results with the SHA1 payload hash stored (and provided) by the server.
The authentication flow is described in "More Details" below.
- At-rest encryption of the log entry payloads. A leak of the entire database should not compromise the users' sync data or passwords.
- Secure encryption of in-flight log entry payloads.
- If an attacker has access to an individual user's sync credentials, they should not be able to access more than that user's data.
- Please see below ("More Details" in particular) on extra steps taken to achieve these goals.
- End-to-end-encryption. While the service only provides sync functionality at the moment, we plan to offer other value-add services that require access to the plaintext data on the server at a later time.
- Encryption at rest on the user's machine.
- An accidental dump of the database should have little value to an attacker, given that "the juicy bits" (the operation log payloads) are encrypted with keys that can not be used with a database dump alone.
- A code injection attack into the sync service. This could be used to exfiltrate the secrets listed below. The sync service contains as little code as possible to reduce this risk and keep its attack surface small. We could further reduce this risk by delegating access to the unencrypted user DEK to a separate microservice, but that is out of scope right now.
- Even if the service accidentally tried to serve an operation log entry for a different user (e.g. by a wrong
SELECTstatement), it should not be able to decrypt and serve that data because the other user's DEK is not available.
- If the attacker gains access to the Kubernetes master, that would be a significant problem, as that would provide them with all secrets required to retrieve and decrypt the database. I'm not sure how to best mitigate this risk. FYI, no other services are currently hosted on that Kubernetes cluster.
- What attack vectors and risks am I missing?
- Are there any serious flaws in this approach? Less-serious flaws?
- Are the assumptions listed at the end of this post correct?
- Are there quick wins to improve the security of this approach?
- Are there parts of the model that are overengineered, i.e. adding complexity without increasing security?
- Would adding Role Based Access Control (or other measures? which ones?) help mitigate the risks to the Kubernetes cluster?
- When adding other features to the web service, are there any generic things to consider besides separating duties by splitting functionality/duties into individual services?
- Would we stand to gain anything in this case by using a "more secure" hash function, such as SHA256? Collisions are of little concern here; we just need to establish basic data integrity.
- Would there be security benefits to using a fully-random IV for each payload and/or even an individual key for each log entry?
- Would there be security benefits to using AES-ECB or AES-GCM in this case, where ciphertext is normally not available to clients?
- Deriving the per-user data encryption key:
- The per-user key is a data encryption key, generated when the user registers and encrypted with a master key encryption key (KEK) stored in Cloud KMS (see Envelope Encryption). Only the encrypted DEK is stored in the PostgreSQL database. The per-user key is decrypted with a call to Cloud KMS whenever it is needed for encrypting or decrypting a payload and discarded after use. Additional authenticated data consisting of the user's ID is provided to Cloud KMS to reduce the risk of confused-deputy attacks. Which user's DEK to fetch and decrypt is determined solely by the user ID provided in the JWT, not the user ID column associated with a particular row in the operation log entry table.
- The master KEK in Cloud KMS is automatically rotated every 3 months, but given that it can not be extracted from Cloud KMS I think this is not very relevant.
- The service authenticates to the Cloud KMS service using service account credentials stored as a secret in Kubernetes.
- Access to the master key is audit logged and the audit logs are monitored for unusual access patterns (more accesses than usual). The master key can not be extracted from Cloud KMS.
- The database is hosted with Cloud SQL and only accessible via a different service account. Those service account credentials as well as the DB password are stored in Kubernetes as well.
- Cloud SQL also provides at-rest encryption by default; our additional encryption scheme is to protect data in the running database as well.
- Admin access to the Google Compute Cloud Console is of course protected with two-factor authentication.
- The Kubernetes cluster itself currently has no extra security provisions besides the defaults provided by Google.
- SSL termination is done by a sidecar Cloud Endpoints Service Proxy instance running in the same Pod as the service itself. All services besides ESP are not accessible directly from the Internet. The SSL certificate (generated via Let's Encrypt) and private key are, you guessed it, in Kubernetes. Private Key pinning is currently not implemented.
- The user registers with an email address and password. The password is hashed via Bcrypt with a random salt and a work factor of 12.
- When the user logs in on the client with email address and password, the service returns a random 24-byte refresh token which is stored in the macOS keychain, so the client does not need to store the user's password.
- For the client to use the service, it send its refresh token to the service, which responds with an HMAC-SHA256-signed JWT (valid for one hour) containing the user ID. This JWT is then used to authenticate to other service methods (upload and download). Staging and production services use different JWT signing keys. The JWT signing key is stored as another Kubernetes secret.
Assumptions / Claims
- Although we are using AES-CBC with a somewhat predictable IV, the predictable should not be an issue because the sync service does not act as an "encryption oracle" — attackers should never have access to the ciphertext.
- The CRIME attack should not apply in this case, as the attacker is not really able to manipulate the compressed payload (the operation log entries). The JWT itself is transferred via HTTP2 which only compresses headers with HPACK. Please correct me if I'm mistaken here.
- Storing the hash of the unencrypted payload in plaintext should not be a problem, as the payloads are fairly long (500+ bytes) and very hard to guess.
Happy to provide more details if needed.