- Kript
Kript is password manager that is secure, convenient, and transparent. It uses a sophistocated encryption scheme than ensures all data is encrypted behind your master password before it leaves your device, guaranteeing that your data is as secure as your master password. Due to your data being synced to the cloud, you can access your passwords from any device anywhere in the world. Additionally, Kript allows users to share passwords securely, allowing you to grant a family member or friend access to an account. And best of all, Kript is open-source, meaning that you can view and even compile the code yourself to ensure that no funny-business is happening with your passwords.
Kript is built using modern tools and architectures. The backend is written in Go, operates as both a gRPC and REST API, and runs within Docker containers on Google Cloud Platform with a micro-service architecture. Currently, the project includes a very basic iOS frontend written in Swift. Additionally, a CI pipeline using Google Cloud Build has been setup for the project, providing GitHub status checks.
Currently, the project includes a very basic iOS frontent written in Swift to serve as an example of how to correctly encrypt and decrypt data and interact with the API. However, from a user perspective, it is missing many features. Contributions of clients, like a better iOS app, an Android app, a website, or a Chrome plugin, are encouraged and would be greatly appreciated.
There is a very large consensus among cybersecurity experts that there are two easy things someone can do to greatly improve their online security: enable multi-factor authentication and use a password manager. Password managers solve a simple problem which has been plaguing cybersecurity for a while, which is that good passwords are hard to remember. As a result, most people reuse passwords, and many of those are easy to guess ones. If humans had perfect memory, we could instead create complex, unique passwords for each account that we have.
Password managers are a convenient solution to this. A password manager is a piece of software that stores passwords for accounts so that they don't need to be memorized. Instead of needing to remember countless passwords, you only need to know one. This one password is called the master password and is used to login to the password manager. A good password manager will keep the passwords secure by encrypting the passwords, ideally with the master password.
Additionally, a good password manager is easy to use. When logging in to a website, it will automatically fill in the password. And when creating an account, it will automatically generate a long, secure password (like Suphut-gyswu6-zisrew
) and save it for future use. Unfortunately, Kript does not yet have autofill functionality, as it only has a proof-of-concept frontend at the moment, and so it is not a viable option as a password manager as it currently stands.
The straightforward answer to this is that you shouldn't. As we all know, strangers on the internet shouldn't be trusted, and that includes me. Passwords are important, and having them stolen can be disastrous. I don't trust anyone with my passwords, and I don't expect anyone else to.
The beauty of Kript, however, is that it doesn't need to be trusted, and this is for two reasons. First, it is designed so that your password never leaves your device. Before any secure data is uploaded to the cloud, it is encrypted, and your master password is the key to decrypt the data. This means that even someone with full access to Kript's servers and databases could not steal your passwords without knowing your master password.
This section describes the protocol used for encrypting "datum"s. In Kript, a "datum" is a piece of data that is meant to be kept secure, like a password, social security number, or private note. These datums are always encrypted and decrypted client side, so understanding the encryption schema is important for properly interacting with the APIs, especially to avoid corrupting data.
This encryption schema was created with the "zero knowledge" principle, which can be described as follows:
- The server and anyone with access to communications between the server and client has zero knowledge of any of the user's secure data. This is achieved by encrypting all secure data with a value only the user knows (their password) before it leaves their device. Thus, no one can steal the user's data unless they get or guess the user's password. This includes me (who has access to the servers and databases), any nefarious person who manages to gain access, or someone who manages to listen in on communications via some sort of man-in-the-middle attack.
Kript uses a mixture of symmetric and asymmetric encryption algorithms. In symmetric-key encryption, the same key is used to encrypt and decrypt a piece of data. In asymmetric-key encryption, there is a public key and a private key that are related to each other, and data is encrypted with the public key and decrypted with the private key. Whenever a symmetric or asymmetric encryption algorithm is used, the client can choose which specific once to use. The list of supported encryption algorithms is found in the protocol definitions at proto/kript/api/universal.proto.
Each of the following sections describe a different component of the encryption schema. Rather than describing how the component is used, these sections describe what the component is and how it uses other components. At the end, there is a diagram demonstrating how they all connect together for clarification, along with some notes about certain design choices. Note that throughout this discussion, a value being stored in an encrypted state on the server also implies that it was transported under the same encryption, as no encryption/decryption happens server-side.
The user's password is the top level key for all access. All decryption paths start using the user's password. The password is never sent to and subsequently stored on the server. Rather, a salted, hashed password is used for authentication so that the password never leaves the user's device. This hashed password is then hashed again with Bcrypt, which is stored in the database. This is necessary due to the initial hash being client-side. The password salt and the hash algorithm are publicly accessible by calling kript.api.AccountService/GetUser
.
Each user has an associated symmetric key that is a hash of the user's password. This value is never sent to and subsequently stored on the server. It should be noted that this is different than the password hash that is used for authentication, which is stored on the server. It uses a different salt and, optionally, a different hash algorithm, which are available by calling kript.api.AccountService/GetUser
. (These values will only be included if it is the currently authenticated user being retrieved.)
Each user is associated with a public key and a private key, and an asymmetric algorithm that those keys are associated with. These are generated client-side when an account is created. The public key is stored unencrypted on the server, but the private key is symmetrically encrypted using the user's symmetric key (from the previous section) client-side. The algorithm that the public and private keys are used for, the public key, the encrypted private key, the initialization vector for encrypting the private key, and the symmetric algorithm used to encrypt the private key can be retrieved by calling kript.api.AccountService/GetUser
. The encrypted private key and the algorithm used to encrypt the private key are only included if the user being retrieved is the currently authenticated user, whereas the rest of the values are publicly available.
Each datum (an individual piece of secure data, like a password) has a key associated with it, along with a symmetric encryption algorithm associated with the key. This key is stored multiple times: once for each user who has access to the datum. (Kript allows for users to share datums with each other). For each user who has access to the datum, the key is encrypted with that user's public key (the one mentioned in the previous section) according the asymmetric algorithm associated with it. Thus, the key can be decrypted with the user's private key. All relevant values are retrieved by calling kript.api.DataService/GetData
.
The core part of each datum is the actual data that contains the secure information. This data is encrypted with the datum's key and its associated symmetric encryption algorithm, as well as an associated initialization vector. These values are all retrieved by calling kript.api.DataService/GetData
.
-
The user's password salt and the hash algorithm used to hash their password are publicly available because these are needed by the client to hash the password before sending it to the server to authenticate the user.
-
There are a number of advantages to having a public and private key associated with each user rather than using their password to encrypt the datum keys:
-
If the user changes their password, the only value that needs to be re-encrypted is their private key. If instead the user's password was used to encrypt datum keys, each datum key would need to be re-encrypted.
-
Having a public key and private key associated with the user allows for asymmetric encryption to be used for the datum keys, whereas using the user's password would require symmetric encryption. The advantage of symmetric encryption is that anyone can encrypt the value using the public key, which is an essential property to enable sharing. If user A wants to share a datum with user B, A simply needs to (on device) decrypt the datum key with their private key and then encrypt it with B's public key. Then, the B-encrypted datum key can be securely uploaded to the server, granting read access to B and B only.
-
Once the client gains authorization from the server, it no longer needs to store the user's password. Rather, it can store the user's private key in order to encrypt and decrypt passwords. This makes it slightly harder for malware on a client to steal the user's master password.
-
-
All encryption happening client-side means that the server is unable to validate encrypted values that are sent to it. Thus, someone creating a client could corrupt data by mis-encrypting it, which would corrupt the data as the server would not know that the data is malformed. Although it would be nice to be able to validate the data, in my research, I found no ways of doing so while retaining the no-knowledge principle.
-
All data is encrypted using the user's password, and the server has no way of knowing the user's password. Therefore, if the user forgets their password, there is no way for a user to recover their data. This is an intrinsic issue of the no-knowledge principle, and thus a necessary disadvantage to maintain security.
Note: The API is no longer available at kript.us
due to cost constraints. To access it, you must instead deploy it yourself.
The Kript API is primarily available via gRPC at grpc.kript.us:443
, and the gRPC interface is described below. The folder proto contains the proto definitions for the gRPC API. These proto files define two services, AccountServer
and DataService
, both of which are available at grpc.kript.us:443
.
Note that this section will make much more sense if the Encryption Schema section is read first.
Kript follows the OAuth model of using refresh tokens and access tokens to authenticate users. The function AccountService.LoginUser
can be used to retreive an access token and refresh token (see Login a user for information on how to do so). Access tokens stay valid for 24 hours and are used to authenticate the user. Refresh tokens, however, stay valid for 100 years and can be used to retreive fresh access tokens by calling AccountService.RefreshAuth
.
- Choose a username and master password for the account
- Choose an asymmetric encryption algorithm to use for encrypting data.
- Choose a symmetric encryption algorithm to use to encrypt the user's private key.
- Choose a hash algorithm to use for hashing the user's password.
- Choose a hash algorithm to use for generating the key to the user's private key.
- Generate a salt for the user's password and hash it via the algorithm chosen in step 4.
- Generate a salt for the key to the user's private key and generate it via the hash algorithm chosen in step 5.
- Generate a public and private key for the user that is appropriate for the algorithm chosen in step 2.
- Generate an initialization vector for encrypting the user's private key that is appropriate for the symmetric algorithm algorithm chosen in step 3.
- Encrypt the user's private key (generated in step 8) using the key generated in step 7 via the symmetric encryption algorithm chosen in step 3 and the initialization vector generated in step 9.
- Call
AccountService.CreateAccount
with the followingCreateAccountRequest
:
{
"username": "...", // the user's username
"password": {
"data": {
"data": ... // the hashed user's password, generated in step 6
}
},
"salt": {
"data": ... // the salt for the user's password hash, generated in step 6
},
"password_hash_algorithm": ..., // the hash algorithm chosen for hashing the user's password, chosen in step 4
"public_key": {
"data": ... // the user's public key, generated in step 8
},
"private_key": {
"data": {
"data": ... // the user's encrypted private key, generated in step 10
}
},
"data_encryption_algorithm": ..., // the algorithm chosen for encrypting the user's data, chosen in step 2
"private_key_encryption_algorithm": ..., // the algorithm chosen for encrypting the user's private key, chosen in step 3
"private_key_iv": {
"data": ... // the initialization vector used for encrypting the user's private key, generated in step 9
},
"private_key_key_salt": {
"data": ... // the salt used for hashing the key to the user's private key, generated in step 7
},
"private_key_key_hash_algorithm": ... // the hashing algorithm for creating the key to the user's private key, chosen in step 5
}
- If creation is successful, the returned message will include the user's refresh token and access token.
For an example, look at the Manager.createAccount
function in Manager.swift.
- Get the user's username and their password.
- Call
AccountService.GetUser
with the user's username to get the user's information. - Hash the user's password. The salt for this, along with the algorithm to use for performing the hash, are included in the data returned in step 2.
- Call
AccountService.LoginUser
with the followingLoginUserRequest
:
{
"username": ..., // the user's username
"password": {
"data": {
"data": ... // the user's hashed password, generated in step 3
}
}
}
- If login is successful and the user does not have two factor authentication set up, the response will include the user's refresh token and access token.
For an example, look at the login
function in Manager.swift.
- Get the user's username and call
AccountService.GetUser
with it. - Create a
Secret
proto message (see encrypt.proto), which will be the encrypted data uploaded. - Encode the proto from step 2 into bytes.
- Choose a symmetric encryption algorithm to use to encrypt the data.
- Generate a key and initialization vector for encrypting the data key, appropriate for the algorithm chosen in step 4.
- Encrypt the proto data from step 3 using the encryption algorithm and key from step 4.
- Encrypt the data key from step 4 using the user's data encryption algorithm and their public key, which are included in the response from step 1.
- Call
AccountService.CreateDatum
using the followingCreateDatumRequest
:
{
"access_token": ..., // the user's access token
"data": {
"data": {
"data": ... // the encrypted data, obtained in step 6
}
},
"data_key": {
"data": {
"data": ... // the encrypted key for the data, generated in step 5
}
},
"data_encryption_algorithm": ..., // the symmetric algorithm for encypting the data, chosen in step 4
"data_iv": {
"data": ... // the initialization vector for encrypting the data, generated in step 5
}
}
For an example, look at the Manager.add
function in Manager.swift.
- Get the username and password for the user.
- Call
AccountService.GetUser
with the user's username to get the user's information. - Re-create the key to the user's private key by hashing their password using the hashing algorithm and salt specified by the
user.private.private_key_key_hash_algorithm
anduser.private.private_key_key_salt
fields of the response from step 2. - Decrypt the user's private key using the key obtained in step 3 via the symmetric encryption algorithm specified by the
user.private.private_key_encryption_algorithm
field of the response from step 2, along with the initialization vector specified atuser.private.private_key_iv
. - Call
DataService.GetData
with the followingGetDataRequest
:
{
"access_token": ... // the user's access token
}
- The response will be a list of
Datum
s. For each, do the following to decrypt them:- Get the datum key that is encrypted behind the user's private key, which is located at
accessors[user_id].data_key
, whereuser_id
is the id of the user (which is located in the response from step 2). - Decrypt the data key from step 6.1 using the user's private key from step 4, via the asymmetric encryption algorithm specified by the
user.public.data_encryption_algorithm
field of the response from step 2. - Decrypt the data at field
data
using the key from step 6.2 and the initialization vector at fielddata_iv
via the symmetric encryption algorithm at fielddata_encryption_algorithm
.
- Get the datum key that is encrypted behind the user's private key, which is located at
For an example, look at the Manager.refresh
function in Manager.swift.
- Obtain the information of the datum to be shared. This can be done by performing the steps from Retrieve saved passwords up to 6.2.
- Call
AccountService.GetUser
with the username of the user to share the datum with. - Encrypt the datum key (from step 6.2 of "Retrieve saved passwords") using the public key of the user to share the data with, via their data encryption algorithm. These values are both located in the response from step 2.
- Choose the permissions to grant the user on the datum.
- Call
DataService.ShareDatum
with the followingShareDatumRequest
:
{
"access_token": ..., // the user's access token
"id": ..., // the id of the datum to share
"target_id": "9ff6edb2-9d06-41dc-b173-8b0865be83f6", // the user id of the user to share the data with, from the reponse in step 2
"data_key": ..., // the data key encrypted using the target user's public key, from step 3
"permissions": // the permission to grant, chosen in step 5
}
There is also a REST API available available at https://api.kript.us
that was generated using gRPC Gateway. Documentation is available for it at server/docs/api/kript.swagger.json. Each REST endpoint corresponds to a gRPC function and vice-versa, so using it is very similar to using the gRPC API. The only difference is in how calls to the API are made.
The below graph provides an overview of the architecture:
Legend:
- Dotted lines represent communication over gRPC.
- Normal lines represent communication over https.
The client has the choice to interact with either the gRPC or REST API, as both are fully featured. While the gRPC connection has a performance advantage, https provides better compatibility.
The gRPC API Service simply merges together the different micro-services (the account and data micro-services) to provide one single endpoint. It can be reached at grpc.kript.us.
The REST API Service also merges together the different micro-services. However, it uses grpc-gateway to provide the API as a REST one. It can be reached at api.kript.us.
The account micro-service is responsible for implementing all functions defined in the kript.api.AccountService service. These are all functions related to account creation, account management, and authorization (logging users in).
The data micro-service is responsible for implementing all functions defined in the kript.api.DataService service. These are all functions related to creating, fetching, sharing, and deleting data. ("data" is referring to a collection of "datum"s. A "datum" in Kript is a piece of secure information, like a password, note, or code.)
The Account Database and Data Database are Cloud Firestore instances that store information for the account and data micro-services, respectively. (This is actually a white lie. They reside in one Firestore instance, as a GCP project is only allowed to have one Firestore instance. However, their data reside in separate "collections" within the instance and are completely separate; neither micro-service ever interacts with the other's data, so they are effectively treated as two separate databases.)
Each of the four services (the gRPC Service, REST Service, Account Service, and Data Service) is containerized using Docker and deployed on Cloud Run, which allows for easy deployment, load-balancing, and autoscaling.
In order to contribute, please open a pull request! To learn about pull requests, look here. Pull requests should be target the development branch, not master.
Kript uses Conventional Commits to format commits in order to keep the history clean. Also, please squash commits on pull requests.
The backend code is located in the [server] folder. To run tests, run make test
. To build the services, run make build
. This will create the directory server/bin
, which will contain executables for each service. If the proto files are edited, run make setup
afterwards to recompile with protoc
. Note that the proto files located at proto
, not server/docs/proto
, should be edited.
The code for each client is located in the directory client
. The code for the proof-of-concept iOS app is at client/ios
and is structured as a typical CocoaPods project. If the proto files are edited, run make setup
afterwards to recompile with protoc
. Note that the proto files located at proto
, not client/docs/proto
, should be edited.
- asymmetric-key encryption: An encryption algorithm that uses one key to encrypt data and a different key to decrypt the same data. The key used to encrypt the data is usually referred to as the public key because it is usually public knowledge. The key used for decryption is usually called the private key. This is usually used when you want to encrypt data for someone else to decrypt. See Wikipedia for more info.
- master password: The password used to login to Kript and encrypt data.
- password manager: A program that stores secure data like passwords.
- private key: In asymmetric-key encryption, the key that is used to decrypt data. It is not publicly known, as that would defeat the purpose of encrypting the data.
- public key: In asymmetric-key encryption, the key that is used to encrypt data. It is usually publicly known since it does not give any ability to decrypt the data.
- symmetric-key encryption: An encryption algorithm that uses the same key to encrypt and decrypt data. This is usually used when you want to encrypt data that only you can decrypt. See Wikipedia for more info.