Hi! I am Abhijeet Nandvikar. I am a Frontend Developer. Recently I was working on a project which required me to implement end-to-end encryption on files before uploading them to cloud storage. This inspired me to write a blog about it to solidify my knowledge and also help other people who are trying to solve a similar problem.
Note: The intention of this blog is just to introduce you to the concept of encryption using Web Crypto API. Do your own research before implementing this in a real project.
What is encryption :
Encryption is the process of taking some data and scrambling it in such a way that its meaning can be derived by only that user who is authorized to do so. Any other user should not understand what this data represents. Data is encrypted and decrypted using a key. One who has the key can only understand the data.
Broadly encryption algorithm is classified into two categories:
Asymmetric encryption algorithms use two types of keys, public and private keys to encrypt and decrypt data. Example: RSA algorithm.
Symmetric encryption algorithms, which use the same key to encrypt and decrypt data. Example: AES, DES, Triple DES algorithm.
Web Crypto API is provided by the browser to implement cryptography on the front end. It’s faster and much safer than using some third-party libraries. It’s also supported by all major browsers, internet explorer only provides partial support. Now let’s take a look at the steps involved to encrypt and decrypt a file.
Getting a password from the user.
Generating a digest from the password.
Generating a key from the digest.
Generating an iv (initialization vector).
Getting a file.
Applying encrypt function.
All the methods that we are going to discuss below will be provided window.crypto object.
STEP 1:
We are going to encrypt a file using a secret, that only the end-user will know. For this, we will take the password as an input.
STEP 2:
The Password taken from the user cannot be used directly as a key for encryption because the Web Crypto API does not allow this. For this, we need to generate a digest first using this password.
What is Digest?
A digest is a value generated by a hash function using a message as input, In our case, the password will be the message.
A digest is usually used to perform different tasks:
generate keys
verify message integrity
store passwords so that they can't be retrieved, but can still be checked
generate pseudo-random numbers etc.
The crypto.subtle.digest() method takes two parameters as inputs, type of algorithm and data. The algorithm can be of the following types SHA-1 (but don't use this in cryptographic applications), SHA-256, SHA-384, SHA-512. Data is an array buffer.
What is an Array Buffer?
The Array Buffer object is used to represent a generic, fixed-length raw binary data buffer. It is an array of bytes, often referred to in other languages as a "byte array".
Therefore we first need to convert our password in the form of an array buffer. For this, we are using TextEncoder Constructor. This converts the string into an array buffer. An enc object has an encoding method that takes a string as an input and outputs an array buffer. An array buffer represents raw data in memory.
export const getDigest = (uid) => {
let enc = new TextEncoder();
return crypto.subtle.digest("SHA-256", enc.encode(uid));
};
This method will return a promise which will be resolved with a digest value in the form of an array buffer.
STEP 3:
Using Digest we are going to generate a key, for this, we use window.crypto.subtle.importKey() method. This method has the following parameters:
Format: It is a string describing the data format of the key to import. Eg : raw pkcs8, jwk, spki etc. Since we are passing array buffer format will be raw
Key Data: key data can be an array buffer or a JSONWebKey Object
Algorithm: It is the name of an encryption algorithm for which we want to generate a key.
Extractable: It is a Boolean indicating whether it will be possible to export the key using SubtleCrypto.exportKey()
Key Usage: It is an array that contains information about the usage of key
export function getKey(value) {
return window.crypto.subtle.importKey("raw", value, "AES-GCM", true, [
"encrypt",
"decrypt"
]);
}
This method returns a promise which is resolved with a Cryptokey object.
STEP 4:
What is an initialization vector?
An initialization vector (IV) is a sequence of random numbers that can be used along with a secret key for data encryption. This number is also called a nonce. The use of an IV prevents repetition in data encryption, making it more difficult for a hacker using a dictionary attack to find patterns and break a cipher.
Here we are going to use window.crypto.getRandomValues() method to generate an iv. We pass an empty byte array of length 12. This method returns a promise which is resolved with an iv.
export const getiv = () => {
return window.crypto.getRandomValues(new Uint8Array(12));
};
STEP 5:
One important thing to note here is that the window.subtle.encrypt() function only accepts data in the form of an array buffer, so we need to read the input file in the form of an array buffer. For this,s we will use the getFile() method that returns a promise which is resolved with an array buffer containing raw file data. The maximum file size that can be handled depends upon the capacity of the device.
// load the file in the memory
export const getFile = async (inputFile) => {
return await inputFile.arrayBuffer();
};
STEP 6:
Now we got all the things necessary to perform encryption on file. Our function takes 3 inputs key, iv, and file. These parameters are then passed to crypto.subtle.encrypt() method.
This method takes 3 parameters:
Algorithm: It is an object specifying the algorithm to be used and any extra parameters if required. Following algorithms are supported in this method RSA-OAEP, AES-CTR, AES-CBC, and AES-GCM.
Key: It is a CryptoKey to be used for encryption.
Data: It is an Array Buffer containing the data to be encrypted.
export const encryptFile = async (key, iv, file) => {
return await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
key,
file
);
};
Encrypt method will return a promise that is resolved with ciphertext, This ciphertext is also in the form of an array buffer. Finally, in order to save our file to local storage or to the cloud, we need to convert it into a blob.
To Decrypt a file we just have to use crypto. subtle.decrypt() method. This method takes in 3 parameters: initialization vector (iv), crypto key, and ciphertext. It returns a promise which is resolved with an array buffer containing the original file.
export const decryptFile = (key,iv, cipherText) => {
return window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
key,
cipherText
);
};
Note: Value of initialization vector(iv) should be the same as used while encryption a file.