Source: keychain/master-key-manager.ts

  1. /**
  2. * @author Erik Haßlmeyer <erik.hasslmeyer@refinio.net>
  3. * @copyright REFINIO GmbH 2022
  4. * @license CC-BY-NC-SA-2.5; portions MIT License
  5. * @version 0.0.1
  6. */
  7. import type {Salt, SymmetricKey} from '../crypto/encryption';
  8. import {
  9. createRandomSalt,
  10. createSymmetricKey,
  11. deriveSymmetricKeyFromSecret,
  12. ensureSalt,
  13. ensureSymmetricKey,
  14. symmetricDecryptWithEmbeddedNonce,
  15. symmetricEncryptAndEmbedNonce
  16. } from '../crypto/encryption';
  17. import {createError} from '../errors';
  18. import {STORAGE} from '../storage-base-common';
  19. import {readPrivateBinaryRaw, writePrivateBinaryRaw} from '../system/storage-base';
  20. import {deleteFile} from '../system/storage-base-delete-file';
  21. /**
  22. * This class encapsulates the master key in such a way that it is harder to be leaked.
  23. *
  24. * The reason why it is a class and not a simple module is, so that we can move it to another
  25. * file without exposing the functions to other modules except the keychain. Other files can use
  26. * it, but they won't be able to decrypt stuff because they don't have access to the object that
  27. * holds the real master key.
  28. *
  29. * The master key is stored in a file that is encrypted. The encryption is done by another key
  30. * that is derived from the secret that is supplied by the user. This derivation needs a salt,
  31. * that also stored in a file. So the loading process of the master key works like this:
  32. * 1) load the salt file
  33. * 2) derive a symmetric encryption key from the secret and the salt
  34. * 3) load the master key file
  35. * 4) decrypt the master key with the derived symmetric key and store it in memory until it is
  36. * unloaded
  37. */
  38. export class MasterKeyManager {
  39. #masterKey: SymmetricKey | null = null;
  40. readonly #masterKeyFileName: string;
  41. readonly #saltFileName: string;
  42. /**
  43. * Constructs a new master key manager.
  44. *
  45. * @param {string} masterKeyFileName - File that stores the encrypted master key
  46. * @param {string} saltFileName - File that stores the salt for deriving the encryption key
  47. * from the secret
  48. */
  49. constructor(masterKeyFileName: string, saltFileName: string) {
  50. this.#masterKeyFileName = masterKeyFileName;
  51. this.#saltFileName = saltFileName;
  52. }
  53. // ######## loading / unloading of master key ########
  54. /**
  55. * Loads the stored master key or create a new one if none was previously created.
  56. *
  57. * This will calculate a derived key from the secret and then:
  58. * - master-key file missing: create a new master-key + file encrypted with this derived key
  59. * - master-key file exists: load the master-key from file and decrypt it with this derived key
  60. *
  61. * Function will throw if the secret does not match the already existing master-key file.
  62. *
  63. * @param {string} secret
  64. * @returns {Promise<void>}
  65. */
  66. public async loadOrCreateMasterKey(secret: string): Promise<void> {
  67. if (this.#masterKey !== null) {
  68. throw createError('KEYMKM-HASKEY');
  69. }
  70. try {
  71. this.#masterKey = await MasterKeyManager.loadAndDecodeMasterKey(
  72. secret,
  73. this.#masterKeyFileName,
  74. this.#saltFileName
  75. );
  76. } catch (e) {
  77. if (e.name !== 'FileNotFoundError') {
  78. throw e;
  79. }
  80. const masterKey = createSymmetricKey();
  81. await MasterKeyManager.writeAndEncodeMasterKey(
  82. secret,
  83. masterKey,
  84. this.#masterKeyFileName,
  85. this.#saltFileName
  86. );
  87. this.#masterKey = masterKey;
  88. }
  89. }
  90. /**
  91. * Purges the memory from memory.
  92. */
  93. public unloadMasterKey(): void {
  94. if (this.#masterKey === null) {
  95. return;
  96. }
  97. this.#masterKey.fill(0);
  98. this.#masterKey = null;
  99. }
  100. /**
  101. * Ensures, that the master is loaded, if not it throws.
  102. */
  103. public ensureMasterKeyLoaded(): void {
  104. if (this.#masterKey === null) {
  105. throw createError('KEYMKM-NOKEY');
  106. }
  107. }
  108. /**
  109. * Changes the secret needed to unlock the master-key.
  110. *
  111. * This can be done with or without a loaded master key. Throws if the oldSecret is wrong.
  112. *
  113. * @param {string} oldSecret
  114. * @param {string} newSecret
  115. * @returns {Promise<void>}
  116. */
  117. public async changeSecret(oldSecret: string, newSecret: string): Promise<void> {
  118. const masterKey = await MasterKeyManager.loadAndDecodeMasterKey(
  119. oldSecret,
  120. this.#masterKeyFileName,
  121. this.#saltFileName
  122. );
  123. await MasterKeyManager.writeAndEncodeMasterKey(
  124. newSecret,
  125. masterKey,
  126. this.#masterKeyFileName,
  127. this.#saltFileName
  128. );
  129. }
  130. // ######## encryption / decryption with master key ########
  131. /**
  132. * Encrypt data with the master key.
  133. *
  134. * Only works if the master key was previously set.
  135. *
  136. * @param {Uint8Array} data
  137. * @returns {Uint8Array}
  138. */
  139. public encryptDataWithMasterKey(data: Uint8Array): Uint8Array {
  140. if (this.#masterKey === null) {
  141. throw createError('KEYMKM-NOKEYENC');
  142. }
  143. return symmetricEncryptAndEmbedNonce(data, this.#masterKey);
  144. }
  145. /**
  146. * Decrypt data with the master key.
  147. *
  148. * Only works if the master key was previously set.
  149. *
  150. * @param {Uint8Array} cypherAndNonce - The data to decrypt
  151. * @returns {Uint8Array}
  152. */
  153. public decryptDataWithMasterKey(cypherAndNonce: Uint8Array): Uint8Array {
  154. if (this.#masterKey === null) {
  155. throw createError('KEYMKM-NOKEYDEC');
  156. }
  157. return symmetricDecryptWithEmbeddedNonce(cypherAndNonce, this.#masterKey);
  158. }
  159. // ######## private section ########
  160. private static async writeAndEncodeMasterKey(
  161. secret: string,
  162. masterKey: SymmetricKey,
  163. masterKeyFileName: string,
  164. saltFileName: string
  165. ): Promise<void> {
  166. const salt = await MasterKeyManager.createSaltFile(saltFileName);
  167. const derivedKey = await deriveSymmetricKeyFromSecret(secret, salt);
  168. const masterKeyEncrypted = symmetricEncryptAndEmbedNonce(masterKey, derivedKey);
  169. await deleteFile(masterKeyFileName, STORAGE.PRIVATE);
  170. await writePrivateBinaryRaw(masterKeyFileName, masterKeyEncrypted.buffer);
  171. }
  172. private static async loadAndDecodeMasterKey(
  173. secret: string,
  174. masterKeyFileName: string,
  175. saltFileName: string
  176. ): Promise<SymmetricKey> {
  177. const salt = await MasterKeyManager.loadSaltFile(saltFileName);
  178. const derivedKey = await deriveSymmetricKeyFromSecret(secret, salt);
  179. const masterKeyEncrypted = new Uint8Array(await readPrivateBinaryRaw(masterKeyFileName));
  180. return ensureSymmetricKey(
  181. symmetricDecryptWithEmbeddedNonce(masterKeyEncrypted, derivedKey)
  182. );
  183. }
  184. private static async createSaltFile(saltFileName: string): Promise<Salt> {
  185. const salt = createRandomSalt();
  186. await deleteFile(saltFileName, STORAGE.PRIVATE);
  187. await writePrivateBinaryRaw(saltFileName, salt.buffer);
  188. return salt;
  189. }
  190. private static async loadSaltFile(saltFileName: string): Promise<Salt> {
  191. const salt = new Uint8Array(await readPrivateBinaryRaw(saltFileName));
  192. return ensureSalt(salt);
  193. }
  194. }