encryptedstorage.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // Copyright 2011 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Provides a convenient API for data persistence with key and
  16. * object encryption. Without a valid secret, the existence of a particular
  17. * key can't be verified and values can't be decrypted. The value encryption
  18. * is salted, so subsequent writes of the same cleartext result in different
  19. * ciphertext. The ciphertext is *not* authenticated, so there is no protection
  20. * against data manipulation.
  21. *
  22. * The metadata is *not* encrypted, so expired keys can be cleaned up without
  23. * decrypting them. If sensitive metadata is added in subclasses, it is up
  24. * to the subclass to protect this information, perhaps by embedding it in
  25. * the object.
  26. *
  27. */
  28. goog.provide('goog.storage.EncryptedStorage');
  29. goog.require('goog.crypt');
  30. goog.require('goog.crypt.Arc4');
  31. goog.require('goog.crypt.Sha1');
  32. goog.require('goog.crypt.base64');
  33. goog.require('goog.json');
  34. goog.require('goog.json.Serializer');
  35. goog.require('goog.storage.CollectableStorage');
  36. goog.require('goog.storage.ErrorCode');
  37. goog.require('goog.storage.RichStorage');
  38. /**
  39. * Provides an encrypted storage. The keys are hashed with a secret, so
  40. * their existence cannot be verified without the knowledge of the secret.
  41. * The values are encrypted using the key, a salt, and the secret, so
  42. * stream cipher initialization varies for each stored value.
  43. *
  44. * @param {!goog.storage.mechanism.IterableMechanism} mechanism The underlying
  45. * storage mechanism.
  46. * @param {string} secret The secret key used to encrypt the storage.
  47. * @constructor
  48. * @struct
  49. * @extends {goog.storage.CollectableStorage}
  50. * @final
  51. */
  52. goog.storage.EncryptedStorage = function(mechanism, secret) {
  53. goog.storage.EncryptedStorage.base(this, 'constructor', mechanism);
  54. /**
  55. * The secret used to encrypt the storage.
  56. *
  57. * @private {!Array<number>}
  58. */
  59. this.secret_ = goog.crypt.stringToByteArray(secret);
  60. /**
  61. * The JSON serializer used to serialize values before encryption. This can
  62. * be potentially different from serializing for the storage mechanism (see
  63. * goog.storage.Storage), so a separate serializer is kept here.
  64. *
  65. * @private {!goog.json.Serializer}
  66. */
  67. this.cleartextSerializer_ = new goog.json.Serializer();
  68. };
  69. goog.inherits(goog.storage.EncryptedStorage, goog.storage.CollectableStorage);
  70. /**
  71. * Metadata key under which the salt is stored.
  72. *
  73. * @type {string}
  74. * @protected
  75. */
  76. goog.storage.EncryptedStorage.SALT_KEY = 'salt';
  77. /**
  78. * Hashes a key using the secret.
  79. *
  80. * @param {string} key The key.
  81. * @return {string} The hash.
  82. * @private
  83. */
  84. goog.storage.EncryptedStorage.prototype.hashKeyWithSecret_ = function(key) {
  85. var sha1 = new goog.crypt.Sha1();
  86. sha1.update(goog.crypt.stringToByteArray(key));
  87. sha1.update(this.secret_);
  88. return goog.crypt.base64.encodeByteArray(sha1.digest(), true);
  89. };
  90. /**
  91. * Encrypts a value using a key, a salt, and the secret.
  92. *
  93. * @param {!Array<number>} salt The salt.
  94. * @param {string} key The key.
  95. * @param {string} value The cleartext value.
  96. * @return {string} The encrypted value.
  97. * @private
  98. */
  99. goog.storage.EncryptedStorage.prototype.encryptValue_ = function(
  100. salt, key, value) {
  101. if (!(salt.length > 0)) {
  102. throw Error('Non-empty salt must be provided');
  103. }
  104. var sha1 = new goog.crypt.Sha1();
  105. sha1.update(goog.crypt.stringToByteArray(key));
  106. sha1.update(salt);
  107. sha1.update(this.secret_);
  108. var arc4 = new goog.crypt.Arc4();
  109. arc4.setKey(sha1.digest());
  110. // Warm up the streamcypher state, see goog.crypt.Arc4 for details.
  111. arc4.discard(1536);
  112. var bytes = goog.crypt.stringToByteArray(value);
  113. arc4.crypt(bytes);
  114. return goog.crypt.byteArrayToString(bytes);
  115. };
  116. /**
  117. * Decrypts a value using a key, a salt, and the secret.
  118. *
  119. * @param {!Array<number>} salt The salt.
  120. * @param {string} key The key.
  121. * @param {string} value The encrypted value.
  122. * @return {string} The decrypted value.
  123. * @private
  124. */
  125. goog.storage.EncryptedStorage.prototype.decryptValue_ = function(
  126. salt, key, value) {
  127. // ARC4 is symmetric.
  128. return this.encryptValue_(salt, key, value);
  129. };
  130. /** @override */
  131. goog.storage.EncryptedStorage.prototype.set = function(
  132. key, value, opt_expiration) {
  133. if (!goog.isDef(value)) {
  134. goog.storage.EncryptedStorage.prototype.remove.call(this, key);
  135. return;
  136. }
  137. var salt = [];
  138. // 64-bit random salt.
  139. for (var i = 0; i < 8; ++i) {
  140. salt[i] = Math.floor(Math.random() * 0x100);
  141. }
  142. var wrapper = new goog.storage.RichStorage.Wrapper(
  143. this.encryptValue_(
  144. salt, key, this.cleartextSerializer_.serialize(value)));
  145. wrapper[goog.storage.EncryptedStorage.SALT_KEY] = salt;
  146. goog.storage.EncryptedStorage.base(
  147. this, 'set', this.hashKeyWithSecret_(key), wrapper, opt_expiration);
  148. };
  149. /** @override */
  150. goog.storage.EncryptedStorage.prototype.getWrapper = function(
  151. key, opt_expired) {
  152. var wrapper = goog.storage.EncryptedStorage.base(
  153. this, 'getWrapper', this.hashKeyWithSecret_(key), opt_expired);
  154. if (!wrapper) {
  155. return undefined;
  156. }
  157. var value = goog.storage.RichStorage.Wrapper.unwrap(wrapper);
  158. var salt = wrapper[goog.storage.EncryptedStorage.SALT_KEY];
  159. if (!goog.isString(value) || !goog.isArray(salt) || !salt.length) {
  160. throw goog.storage.ErrorCode.INVALID_VALUE;
  161. }
  162. var json = this.decryptValue_(salt, key, value);
  163. try {
  164. wrapper[goog.storage.RichStorage.DATA_KEY] = goog.json.parse(json);
  165. } catch (e) {
  166. throw goog.storage.ErrorCode.DECRYPTION_ERROR;
  167. }
  168. return wrapper;
  169. };
  170. /** @override */
  171. goog.storage.EncryptedStorage.prototype.remove = function(key) {
  172. goog.storage.EncryptedStorage.base(
  173. this, 'remove', this.hashKeyWithSecret_(key));
  174. };