/key-store

A TypeScript Node.JS library that provides a basic key store for data encryption keys with 0 dependencies outside of dev dependencies

Primary LanguageTypeScriptThe UnlicenseUnlicense

Key-Store

Quality Gate Status Coverage Security Rating Vulnerabilities Code Smells Bugs

What is this?

This is a typescript library that can be used to create a key store for managing root and data encryption keys. A basic file store key store is included but it can also be extended to persist to a shared store such as redis. What problem does this solve? This was intiailly created as part of a data munging project in which I needed to be able to dynamically encrypt credentials and share them across a distributed system using redis.

How do I use it?

To create your own store extend from the BaseKeyStore and implement the required key slot functions. For example:

import { writeFile, mkdir, access, readFile, unlink, rm } from 'fs/promises'
import { resolveHome } from './resolve.js'
import {
  BaseKeyStore,
  IKeyStoreContextProvider,
  IKeyStoreValueProvider,
} from './baseKeyStore'

/**
 * A implementation of BaseKeyStore that stores the sealed
 * keys into a desiginated spot in the file system, consumers
 * of the class supply providers that give the password, salt
 * and context to use for AAED
 */
export class FileKeyStore extends BaseKeyStore {
  private readonly keyStorePath: string

  /**
   *
   * @param {string} keyStorePath path to the folder where keys will be saved
   * @param {IKeyStoreValueProvider} keyStorePasswordProvider provide the password used to seal keys
   * @param {IKeyStoreValueProvider} keyStoreSaltProvider provide the salt used to seal keys
   * @param {IKeyStoreContextProvider} keyStoreContextProvider provider that will give the appropriate context based on key id
   */
  constructor(
    keyStorePath: string,
    keyStorePasswordProvider: IKeyStoreValueProvider,
    keyStoreSaltProvider: IKeyStoreValueProvider,
    keyStoreContextProvider: IKeyStoreContextProvider
  ) {
    super(
      keyStorePasswordProvider,
      keyStoreSaltProvider,
      keyStoreContextProvider
    )
    this.keyStorePath = resolveHome(keyStorePath)
  }

  /**
   * @Inheritdoc
   */
  protected hasKeyInSlot(keySlot: string): Promise<boolean> {
    return access(this.keyStorePath + '/' + keySlot)
      .then(() => true)
      .catch(() => false)
  }

  private async createKeyStoreDirIfNotExists(): Promise<void> {
    await access(this.keyStorePath).catch(async () => {
      await mkdir(this.keyStorePath, { recursive: true })
    })
  }

  /**
   * @Inheritdoc
   */
  protected async putKeyInSlot(keySlot: string, key: Buffer): Promise<void> {
    await this.createKeyStoreDirIfNotExists()
    await writeFile(this.keyStorePath + '/' + keySlot, key)
  }

  /**
   * @Inheritdoc
   */
  protected async getKeyInSlot(keySlot: string): Promise<Buffer> {
    return readFile(this.keyStorePath + '/' + keySlot)
  }

  /**
   * @Inheritdoc
   */
  protected async deleteKeySlot(keySlot: string): Promise<void> {
    await unlink(this.keyStorePath + '/' + keySlot)
  }

  /**
   * @Inheritdoc
   */
  protected async clearKeySlots(): Promise<void> {
    await rm(this.keyStorePath, { recursive: true, force: true, maxRetries: 3 })
  }

  /**
   * @Inheritdoc
   */
  async close(): Promise<void> {
    // nothing to do
  }
}

You can then use the store like this (snippet from a test):

const storeDir = tmpdir()
const key = randomBytes(32)
const salt = randomBytes(16)
const context = randomBytes(32)

// create a keystore
const keystore = new FileKeyStore(
  storeDir + '/keystore',
  () => Promise.resolve(key),
  () => Promise.resolve(salt),
  () => Promise.resolve(context)
)

// create random data to act as key store
const dek = randomBytes(32)
const id = randomUUID()

// save it
await keystore.saveSealedDataEncKey(id, dek)

// ask for it back
const fetchedDek = await keystore.fetchSealedDataEncKey(id)

// should be the same
expect(fetchedDek).toEqual(dek)