KMS (Key Management Service) is a service for managing cryptographic encryption keys. There are some KMSs in the world, one of which is provided by Google as a part of their Cloud Platform. This tutorial shows you how to use Google Cloud KMS with Node.js as the client, including how to get credentials, managing KeyRing and CryptoKey as well as encrypting and decrypting data using the CryptoKeys
Why Use KMS?
Sometimes you want to protect your data by encrypting it. Of course you need a cryptographic key for the encryption. With KMS, generating key is very simple and secure. Most KMSs use strong standard for encryption. For example, Google KMS uses AES-256. Your keys will be stored on their server and you can use the keys anywhere, no need to store it on your portable drive - which will be dangerous if you lost it and being misused by someone. In addition, they also provide automatic key rotation, which means you don't need to manually rotate the key.
About Google KMS
The Google's Cloud Key Management Service uses an object hierarchy. Here is the hierarchy from top to bottom:
1. Project
Like using other Google Cloud services, you need a project. It means your resources must belong to a project.
2. Location
You can choose where your keys will be stored. It represents the geographical locations where your resources are stored. If you choose global, your resources will be available from multiple data centers.
3. Key Ring
The keys are grouped into key ring. A key ring belongs to a project and resides in a particular locations. Key ring makes it easy to manage keys. You can group related keys together in a key ring and you can apply bulk operations such as grant, revoke, or modify to all the keys in the key ring. The resource ID of a key ring is represented in the from:
projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]
4. Key
The key itself is a crytographic key used for encryption. The resource ID of a key is represented in the form:
projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]
5. Key Version
A key can have multiple versions. You can set rotation interval when the next key version should be generated.
Getting Credentials
First you need to create credentials that will be used by your app.
1. Create or Select A Google Cloud Project
Open Google Cloud Console. You can either use an existing project or create a new one.
2. Enable Billing Account For The Project
If you haven't enabled billing for your project, it must be enabled first. Go to Projects Billing page of Google Cloud Console.
3. Enable Cloud Key Management Service (KMS) API
Open Google API Dashboard Click on library on the menu, then search for KMS. On the result, you should see Cloud Key Management Service (KMS) API, click on it. On the opened page, click on enable button.
4. Create OAuth Client ID
Your application needs credential to get authenticated by Google. To manage credentials, open the credentials page. Click on create credentials button and choose OAuth client id.
If you haven't set a product name on the consent screen, you'll be asked to set it up first. Click on the configure consent screen button. You'll be redirect to a page for setting OAuth consent screen. Among the fields on that page, you only need to fill at least email address and product name shown to users.
Then, you'll be redirected back to the create OAuth client ID page. Choose web application for application type and enter the name for the client ID. The authorized JavaScript origins section is not used for now. For authorized redirect URL, you can fill it with any URL (when you allow your application to access your account, you'll be redirected to that URL). Lastly, click on create button.
If it's successful, you'll be redirected to credentials list page. On the credential you've just created, click the download icon.
Then open the downloaded file. Search for the value of client_id, project_id, client_secret and redirect_uris. Based on those values, add the followings to your .env file
GOOGLE_CLOUD_OAUTH_CLIENT_ID=11223344556-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com # replace with client_id
GOOGLE_CLOUD_OAUTH_PROJECT_ID=abcdef-abcdefg-123456 # replace with project_id
GOOGLE_CLOUD_OAUTH_CLIENT_SECRET=-aBcdEfGHijKlmnoPQRSTuVw # replace with client_secret
GOOGLE_CLOUD_OAUTH_REDIRECT_URL=https://www.woolha.com # replace with the first element of redirect_uris
Dependencies
In this tutorial, the following dependencies need to be added. Add them to your package.json and run npm install
"dotenv": "~4.0.0"
"googleapis": "~32.0.0"
Getting Tokens
For authentication using OAuth, we need to have the tokens in order to get authenticated. Access token is used for every requests sent. However, it only last for 1 hour before expires. Refresh token will be used to get the new access token. Fortunately, the OAuth2 class (google.auth.OAuth2) of googleapis module has already support token renewal. So, we only need to supply refresh token and it will do the renewal process automaticaly if the token expires.
The OAuth2 class has a method getAccessToken for getting the access token for a given code. Of course you need to get the code first. The same class has generateAuthUrl method, which helps you to get the code by generating a URL for authentication. The script below uses generateAuthUrl for generating the URL.
require('dotenv').config();
const { google } = require('googleapis');
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLOUD_OAUTH_CLIENT_ID,
process.env.GOOGLE_CLOUD_OAUTH_CLIENT_SECRET,
process.env.GOOGLE_CLOUD_OAUTH_REDIRECT_URL,
);
const KMS_SCOPES = 'https://www.googleapis.com/auth/cloudkms';
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: KMS_SCOPES,
});
console.info(`authUrl: ${url}`);
Open the URL on any web browser. You may need to login or if you've multiple accounts, select the account you want to use to manage KMS. Then a consent screen will be shown and click on the allow button.
After that, you'll be redirected to the URL you've set as the value of GOOGLE_OAUTH_REDIRECT_URL. The URL should include a query string named code, copy the value (without the # symbol at the end) - that's your code.
https://www.woolha.com/?code=4/AABBCC-abcdEFGH1-aBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDe#
Having obtained the code, now we're going to get the tokens by running this script.
require('dotenv').config();
const { google } = require('googleapis');
// Replace with the code you've got on
const code = '4/AABBCC-abcdEFGH1-aBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDeaBcDe';
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLOUD_OAUTH_CLIENT_ID,
process.env.GOOGLE_CLOUD_OAUTH_CLIENT_SECRET,
process.env.GOOGLE_CLOUD_OAUTH_REDIRECT_URL,
);
const getToken = async () => {
const { tokens } = await oauth2Client.getToken(code);
console.info(tokens);
};
getToken();
After you run that script, you should get the tokens.
{
access_token: 'abcd.abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCD',
token_type: 'Bearer',
refresh_token: '1/abc123abc123abc123abc123abc123abc123abc123a',
expiry_date: 1529136753542
}
On your .env, add the followings
GOOGLE_CLOUD_OAUTH_REFRESH_TOKEN=1/abc123abc123abc123abc123abc123abc123abc123a # replace with refresh_token
GOOGLE_CLOUD_OAUTH_ACCESS_TOKEN=abcd.abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCDef12abCD # replace with access_token
GOOGLE_CLOUD_OAUTH_TOKEN_EXPIRE=1529136753542 # replace with expiry_date
As we've got the tokens, now we can start to try KMS. First we create an object of cloud KMS client
helpers/google-cloud-kms.js
const _ = require('lodash');
const google = require('googleapis').google;
function KMSClient() {
if (!(this instanceof KMSClient)) {
return new KMSClient();
}
this.projectId = process.env.GOOGLE_CLOUD_PROJECT_ID;
this.location = 'global';
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLOUD_OAUTH_CLIENT_ID,
process.env.GOOGLE_CLOUD_OAUTH_CLIENT_SECRET,
process.env.GOOGLE_CLOUD_OAUTH_REDIRECT_URL,
);
oauth2Client.setCredentials({
refresh_token: process.env.GOOGLE_CLOUD_OAUTH_REFRESH_TOKEN,
expiry_date: process.env.GOOGLE_CLOUD_OAUTH_TOKEN_EXPIRE,
access_token: process.env.GOOGLE_CLOUD_OAUTH_ACCESS_TOKEN,
token_type: 'Bearer',
});
// Instantiates an authorized client
const cloudkms = google.cloudkms({
version: 'v1',
auth: oauth2Client
});
this.cloudkmsClient = cloudkms;
}
// Add the methods on the usage examples below here
module.exports = KMSClient
We create an example file that uses it.
example.js
const KMSClient = require('./helpers/google-cloud-kms');
const client = new KMSClient();
The followings are usage examples of some methods on googleapis/build/src/apis/cloudkms/v1.js. Please read the JSDoc above each methods in order to know the params you need to pass when calling the methods.
Create a KeyRing
To create a key ring, you need to provide the parent (location) in the format of projects/[PROJECT_ID]/locations/[LOCATION] and the key ring identifier which must be unique within the location.
helpers/google-cloud-kms.js
/**
* Create a new KeyRing in a given Project and Location.
* @param {Object} params
* @param {string} params.keyRingId - It must be unique within a location and match the regular expression `[a-zA-Z0-9_-]{1,63}`.
* @param {string} [params.location] - Custom location.
* @return {Promise}
*/
KMSClient.prototype.createKeyRing = function (params) {
const _params = {
keyRingId: params.keyRingId,
parent: `projects/${this.projectId}/locations/${params.location || this.location}`,
};
return this.cloudkmsClient.projects.locations.keyRings.create(_params);
};
example.js
client.createKeyRing({ keyRingId: 'key-ring-test-1' });
Get The List of KeyRings
To get the list of key rings, you need to provide the parent (location) in the format of projects/[PROJECT_ID]/locations/[LOCATION]. Optionally, you can also implement pagination using pageSize and pageToken whose value is obtained from next_page_token of the previous response.
helpers/google-cloud-kms.js
/**
* Lists KeyRings.
* @param {Object} params
* @param {number} [params.pageSize] - Limit on the number of KeyRings to include in the response.
* @param {string} [params.pageToken] - Optional pagination token. Obtained from next_page_token of previous response.
* @param {string} [params.location] - Custom location.
* @return {Promise}
*/
KMSClient.prototype.listKeyRing = function (params) {
const _params = {
parent: `projects/${this.projectId}/locations/${this.location}`,
pageSize: params.pageSize,
pageToken: params.pageToken,
location: params.location,
};
return this.cloudkmsClient.projects.locations.keyRings.list(_params)
.then(result => result.data.keyRings || []);
};
example.js
client.listKeyRing().then(console.log);
Get Details of a KeyRing
To get the metadata of a key ring, you need to provide the name of the key ring in the format of projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING].
/**
* Returns metadata for a given KeyRing.
* @param {Object} params
* @param {string} params.keyRingName - The name of the KeyRing.
* @param {string} [params.location] - Custom location.
* @returns {Promise}
*/
KMSClient.prototype.getKeyRing = function (params) {
const _params = {
name: `projects/${this.projectId}/locations/${params.location || this.location}/keyRings/${params.keyRingName}`,
};
return this.cloudkmsClient.projects.locations.keyRings.get(_params)
.then(result => result.data);
};
example.js
client.getKeyRing({ keyRingName: 'gcs-demo-key-ring' }).then(console.log);
Create a CryptoKey
To create a crypto key, you need to provide the parent (key ring) in the format of projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING] and the name of the crypto key. The key purpose is required as well.
helpers/google-cloud-kms.js
/**
* Create a new CryptoKey within a KeyRing.
* @param {Object} params
* @param {string} params.cryptoKeyId - It must be unique within a KeyRing and match the regular
* expression `[a-zA-Z0-9_-]{1,63}`.
* @param {string} params.parent - The name of the KeyRing associated with the CryptoKeys.
* @param {Object} params.resource - See the format on https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys#CryptoKey
* @param {string} params.resource.purpose - Purpose of the CryptoKey.
* @param {string} [params.resource.nextRotationTime] - The time when the KMS will create a new version of the key and mark it as primary.
* @param {string} [params.resource.labels] - Labels with user-defined metadata in the form of Map (key: string, value: string).
* @param {string} [params.resource.rotationPeriod] - nextRotationTime will be advanced by this period when the service automatically rotates a key. Must be at least one day.
* @param {string} [params.location] - Custom location.
* @returns {Promise}
*/
KMSClient.prototype.createCryptoKey = function (params) {
const _params = {
cryptoKeyId: params.cryptoKeyId,
parent: `projects/${this.projectId}/locations/${params.location || this.location}/keyRings/${params.keyRingName}`,
resource: params.resource
};
return this.cloudkmsClient.projects.locations.keyRings.cryptoKeys.create(_params)
};
example.js
const labelsMap = new Map();
labelsMap.set('abc', 'def');
labelsMap.set('ghi', 'jkl');
client.createCryptoKey({
keyRingName: 'key-ring-test-1',
cryptoKeyId: 'crypto-key-test-3',
resource: {
purpose: 'ENCRYPT_DECRYPT', // must be CRYPTO_KEY_PURPOSE_UNSPECIFIED or ENCRYPT_DECRYPT
nextRotationTime: moment(new Date()).add(3, 'months').toISOString(),
labels: labelsMap,
rotationPeriod: (30 * 86400) + 's',
}
});
Get The List of CryptoKeys
To get the list of crypto keys, you need to provide the parent (key ring) in the format of projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]. Optionally, you can also implement pagination using pageSize and pageToken whose value is obtained from next_page_token of the previous response.
helpers/google-cloud-kms.js
/**
*
* @param {Object} params
* @param {Object} params.keyRingName - The name of the KeyRing.
* @param {Object} [params.pageSize] - Limit on the number of KeyRings to include in the response.
* @param {Object} [params.pageToken] - Optional pagination token. Obtained from next_page_token of previous response.
* @param {string} [params.location] - Custom location.
* @return {Promise}
*/
KMSClient.prototype.listCryptoKey = function (params) {
const _params = {
parent: `projects/${this.projectId}/locations/${params.location || this.location}/keyRings/${params.keyRingName}`,
pageSize: params.pageSize,
pageToken: params.pageToken,
};
return this.cloudkmsClient.projects.locations.keyRings.cryptoKeys.list(_params)
.then(result => result.data);
};
example.js
client.listCryptoKey({
keyRingName: 'key-ring-test-1',
});
Get Details of a CryptoKey
To get the metadata for a crypto key, you need to provide the name in the format of projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY].
helpers/google-cloud-kms.js
/**
* Returns metadata for a given CryptoKey.
* @param {Object} params
* @param {string} params.keyRingName - The name of the KeyRing.
* @param {Object} params.requestBody
* @param {string} params.requestBody.plaintext - The Base64 of data you want to encrypt.
* @param {string} [params.location] - Custom location.
* @return {Promise}
*/
KMSClient.prototype.getCryptoKey = function (params) {
const _params = {
name: `projects/${this.projectId}/locations/${params.location || this.location}/keyRings/${params.keyRingName}/cryptoKeys/${params.cryptoKeyId}`,
};
return this.cloudkmsClient.projects.locations.keyRings.cryptoKeys.get(_params);
};
example.js
client.getCryptoKey({
keyRingName: 'gcs-demo-key-ring',
cryptoKeyId: 'gcs-demo-key-1',
}).then(console.log);
Encrypt Data
To encrypt data, you need to provide the name of the key in the format of projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY] or the key version in the format projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]/cryptoKeyVersions/[VERSION]. The plaintext must be in base64 format.
helpers/google-cloud-kms.js
/**
* Encrypts data
* @param {Object} params
* @param {string} params.keyRingName - The name of the KeyRing.
* @param {string} params.cryptoKeyId - The name of the CryptoKey.
* @param {number} params.[cryptoKeyVersion] - The version of the CryptoKey.
* @param {Object} params.requestBody
* @param {string} params.requestBody.plaintext - The Base64 of data you want to encrypt.
* @param {string} [params.location] - Custom location.
* @return {Promise}
*/
KMSClient.prototype.encrypt = function (params) {
let name =`projects/${this.projectId}/locations/${params.location || this.location}/keyRings/${params.keyRingName}/cryptoKeys/${params.cryptoKeyId}`;
if (params.cryptoKeyVersion) {
name += `/cryptoKeyVersions/${params.cryptoKeyVersion}`;
}
const _params = {
name,
requestBody: params.requestBody,
};
return this.cloudkmsClient.projects.locations.keyRings.cryptoKeys.encrypt(_params);
};
example.js
client.encrypt({
keyRingName: 'key-ring-test-1',
cryptoKeyId: 'crypto-key-test-3',
cryptoKeyVersion: 1,
requestBody: {
plaintext: Buffer.from('Hello World').toString('base64'),
}
})
.then(result => {
console.log(result.data);
});
Decrypt Data
To decrypt data, you need to provide the name of the key in the format of projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY] or the key version in the format of projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]/cryptoKeyVersions/[VERSION], and of course the ciphertext to be decrypted. The result is in base64 format.
helpers/google-cloud-kms.js
/**
* Decrypts data
* @param {Object} params
* @param {string} params.keyRingName - The name of the KeyRing.
* @param {string} params.cryptoKeyId - The name of the CryptoKey.
* @param {number} params.[cryptoKeyVersion] - The version of the CryptoKey.
* @param {Object} params.requestBody
* @param {string} params.requestBody.ciphertext - The ciphertext you want to decrypt.
* @param {string} [params.location] - Custom location.
* @return {Promise}
*/
KMSClient.prototype.decrypt = function (params) {
let name =`projects/${this.projectId}/locations/${params.location || this.location}/keyRings/${params.keyRingName}/cryptoKeys/${params.cryptoKeyId}`;
if (params.cryptoKeyVersion) {
name += `/cryptoKeyVersions/${params.cryptoKeyVersion}`;
}
const _params = {
name,
requestBody: params.requestBody,
};
return this.cloudkmsClient.projects.locations.keyRings.cryptoKeys.decrypt(_params)
.then(result => Buffer.from(result.data.plaintext, 'base64').toString());
};
example.js
client.decrypt({
keyRingName: 'key-ring-test-1',
cryptoKeyId: 'crypto-key-test-3',
requestBody: {
ciphertext: 'CiQAGxP6hGSRDMBBT5ylpTSIbjvmQYsaYkQGqDR1VyOqTN7CGSESNABABpElbvmcCvW9QTTDRJaiNxzDVT5HxR9Lr4Nla0ZGF+e4SvJGnwmhZygZR/OjiDgID9M=',
}
})
.then(console.log);
The examples above are only a few of cloudkms methods. For the list of all available methods along with the documentation, you can read googleapis/build/src/apis/cloudkms/v1.d.ts.