@raytio/maxcryptor
v3.1.0
Published
## What is the Maxcryptor?
Downloads
308
Keywords
Readme
Maxcryptor
What is the Maxcryptor?
The Maxcryptor was developed for rayt.io for use in the web client. It has been made open source primarily for the purpose of transparency and auditing of the cryptography used by rayt.io, but there's no reason it can't be used for other projects.
It exposes a "safe" set of cryptographic functions that can be used to encrypt, decrypt, sign, verify and share data, without the user of the library having to worry about getting the crypto correct.
How does it work?
The Maxcryptor uses the browser's built in Web Crypto API under the hood. As a result it is only supported by browsers that support the spec.
The two main entities and their operations are described below.
User
A user is designed to be a single user account. A user can perform the following operations:
- Encrypt JSONable data
- Decrypt data encrypted by the encrypt operation
- Update data which has already been encrypted
- Share data encrypted by the encrypt operation with an application
- Sign data so that other parties can verify it
- Verify data was signed by themself
- Verify data was signed by another user or application
- Create application for receiving shared data
- Load application that they have created, or has been shared with them
Application
An application is an entity designed to be used by many users.
Users can share data with an application.
Users who have been granted access to an application can:
- Decrypt data shared with the application
- Share data that has been shared with this application with another application
- Share access to that application
What crypto does it use?
A user has two key pairs, one for encryption/decryption and one for signing/verifying. The public keys are stored unencrypted in JWK format, and the private keys are encrypted with AES-GCM with a key length of 256 bits. The key used to encrypt the private keys is derived from the user's password using PBKDF2 with a hash of SHA-512 and 10000 iterations. A separate IV is used for each encrypted key, and is stored with the encrypted keys.
Data is encrypted with AES-GCM with a key length of 256 bits. A new key is generated for each piece of data that is encrypted. This key is then encrypted with RSA-OAEP with a modulus length of 2048 bits, and SHA-512 as the hash function. The advantage of this is that the data doesn't have to be re-encrypted to be shared, the key can just be re-encrypted with the public key of the party which the data is to be shared with. Note that unless the data is signed before being encrypted, or it is enforced with application logic the encrypted data could be modified by any of the parties that the data has been shared with.
Signing is performed with RSA-PSS with a modulus length of 2048 bits, and a hash of SHA-512. JSON structures are first serialized with a canonical JSON spec as described here: http://gibson042.github.io/canonicaljson-spec/ , then encoded in UTF-8 to ensure that semantically equivalent data will always verify correctly.
Crypto within an application is identical to a user in every way other than how it's private key is stored. Rather than belonging to a single user, an application can belong to several. The application's private key is encrypted exactly like data, (first AES, then RSA) so that it can be shared.
Known issues
- Because we don't sign data before encrypting it, there is no way to verify that encrypted data was created by a specific party, only that they had access to the key
- We don't have a key chain, so signing and verifying data is of limited use. You would have to trust that the public key belongs to the user or application that you think it does.
- While some work has been done to enable upgrading ciphers as time passes, this is far from complete
- We don't currently have a way to cycle keys, and this will have to be done in application code if it is desired
Which browsers are supported?
Any browser that supports the relevant parts of the Web Crypto API spec, and several other browser specs.
This includes Chrome, Firefox and Safari. Most common mobile browsers should be supported, however extensive testing has not yet been performed.
Internet Explorer is not supported as it only has partial support for the Web Crypto API.
Edge is not supported as it is missing the Text Encoder API, however this could easily be polyfilled.
What about polyfilling unsupported browsers?
In theory this could be done, but it would be very difficult to verify that any polyfill is not compromised. For this reason it is unlikely that we will ever support browsers without the WebCrypto API.
How do I use it?
Firstly you'll want to create a new user:
Creating a user encryptor
import { newUser } from '@raytio/maxcryptor';
const { encryptor, userDoc } = await newUser('my very secure password');
This returns an encryptor and some userDoc. You'll want to store user data somewhere so that the encryptor can be retrieved again later.
The userDoc is a JSON object, with the structure:
{
kek_derivation_config: KeyDerivationConfigParams;
private_key_encryption_config: KeyEncryptionConfigParams;
encryption_key_pair: EncryptedKeyPair;
signing_key_pair: EncryptedKeyPair;
}
Getting an encryptor for an existing user
The encryptor can be retrieved from the data we saved when we created a new user, and the password that was used to create it.
import { encryptorFromExistingUser } from '@raytio/maxcryptor';
const { encryptor } = await encryptorFromExistingUser(userDoc, 'my very secure password');
Supporting "Remember Me" login persistence
If we want to support "Remember Me" login persistence then we have to store enough information to reconstruct an encryptor at a future date. We could store: 1. The user's password 2. The key (kek) used to decrypt the user's encryption and signing keys 3. The encryption and signing keys themselves
While all of these situations are likely to lead to encrypted data being read if the persisted authentication data is compromised, there are reasons why options 1 and 3 are worse: - If the persisted data is compromised in option 1, the user's password (which is used for API access, and may be used for other services, even though it discouraged) is compromised. - In option 3 there is a lot more data to store, which could theoretically exceed what is allowed the persistence mechanism. - In option 3 we have to ensure that the authentication data that is persisted is up to date. - If the persisted data is compromised in option 3, then encrypted data will be readable even after a password change.
When we first log the user in, we'll want to store the kek:
import { encryptorFromExistingUser } from '@raytio/maxcryptor';
const { encryptor, kek } = await encryptorFromExistingUser(userDoc, 'my very secure password');
// In this example we'll save to local storage
localStorage.setItem('kek', kek);
When the user returns, we'll load the kek and build our encryptor:
import { encryptorFromExistingUserKek } from '@raytio/maxcryptor';
const kek = localStorage.getItem('kek');
const { encryptor } = await encryptorFromExistingUserKek(userDoc, kek);
When the user logs out it is very important that we discard the kek:
const kek = localStorage.removeItem('kek');
Changing a user's password
This works by decrypting their keys with their existing password, and re-encrypting them with the new one. This means no pasword reset is possible! You'll want to save the new userDoc, as the existing data is not modified.
import { changePassword } from '@raytio/maxcryptor';
const newUserDoc = await changePassword(
userDoc,
'my very secure password',
'my much more secure password'
);
Exporting a user's keys
A user can export their keys to JSON so that they can recover their data even if they forget their password. These keys will need to be kept very secure.
import { exportKeys } from '@raytio/maxcryptor';
const exportedKeys = await exportKeys(
userDoc,
'my very secure password'
);
Importing a user's keys
In the case when the user forgets their password but has exported their keys, they can restore their user document with a new password, and still have access to their previously encrypted data.
import { importKeys } from '@raytio/maxcryptor';
const newUserDoc = await importKeys(
exportedKeys,
'i hope i don\'t forget this password!'
);
Encrypting and decrypting a user's own data
What good is this library if you can't encrypt or decrypt data? Any JSONable data can be encrypted with the encryptor you get for a new or existing user.
const dataToEncrypt = "string, but this could be any jsonable data type";
const encryptedData = await encryptor.encrypt(dataToEncrypt);
const decryptedData = await encryptor.decrypt(encryptedData);
console.log(dataToEncrypt === decryptedData); // Will print true
Updating existing encrypted data
A user can "update" their encrypted data. This is useful when the original data has been shared, and the user wants these changes to be visible to applications which the data has been shared with. If this is not desirable then use encryptor.encrypt() instead.
Note: This won't modify the input data, but rather return a new encrypted data object.
const dataToEncrypt = "string, but this could be any jsonable data type";
const originalEncryptedData = await encryptor.encrypt(dataToEncrypt);
const newDataToEncrypt = "something different";
const newEncryptedData = await encryptor.update(
newDataToEncrypt,
originalEncryptedData
);
Creating an application
An application is an entity that users can share data with. We use an ApplicationEncryptor, which is very similar to a regular DataEncryptor, but with a slightly different set of operations. You'll want to export the public key so that other users can share data with it, and encrypt the private key so that you can decrypt data that has been shared with it.
Notice that you need to supply a public key when encrypting the private key. This allows you to share them with other users, allowing multiple users to decrypt the shared data.
const applicationEncryptor = await encryptor.createApplicationEncryptor();
// Share this with anyone you want to be able to share data with
const applicationPublicKey = await applicationEncryptor.exportPublicKey();
// Only users that you encrypt the private key for will be able to load the
// application for decryption. This includes your user!
const applicationEncryptedPrivateKey = await applicationEncryptor.encryptPrivateKey(
// Note that we encrypt with the user's public key
userDoc.encryption_key_pair.public_key
);
Sharing data with an application
If you wish to share data with an application, it must first be encrypted. You will then receive a key that can be used by the application (and only that application) to decrypt that data.
const dataToEncrypt = "string, but this could be any jsonable data type";
const encryptedData = await encryptor.encrypt(dataToEncrypt);
const encryptedDecryptionKey = await encryptor.share(
encryptedData,
applicationPublicKey
);
Loading an application for decryption
If you wish to obtain an instance of the application that you have created previously, or that has been shared with you, you can do so with your user encryptor, the application's public key, and the private key that has been encrypted for your user.
const applicationEncryptor = await encryptor.loadApplicationEncryptorForDecryption(
applicationPublicKey,
applicationEncryptedPrivateKey
);
Decrypting data with an application
Now that you have the application and some data shared with it, you can decrypt it.
const decryptedData = await applicationEncryptor.decrypt(
encryptedData,
encryptedDecryptionKey
);
console.log(dataToEncrypt === decryptedData); // Will print true
On-sharing data shared with an application
An application can shared data which has been shared with it on to another application. The recipient application can decrypt as it would if it had been shared directly with them.
Note: This operation is identical to the share operation on the user encryptor, other than taking a third argument.
const encryptedDecryptionKey = await applicationEncryptor.share(
encryptedData,
application2PublicKey,
encryptedDecryptionKey
);
Other operations
Both user and application encryptors are capable of other operations, however they aren't documented because we don't currently use them and they are likely to change.
License
This project is under the MIT license, as documented here: https://www.rayt.io/mit