keychain.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { arrayToB64, b64ToArray } from './utils';
  2. import ECE from './ece.js';
  3. const encoder = new TextEncoder();
  4. const decoder = new TextDecoder();
  5. export default class Keychain {
  6. constructor(secretKeyB64, nonce, ivB64) {
  7. this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
  8. if (ivB64) {
  9. this.iv = b64ToArray(ivB64);
  10. } else {
  11. this.iv = crypto.getRandomValues(new Uint8Array(12));
  12. }
  13. if (secretKeyB64) {
  14. this.rawSecret = b64ToArray(secretKeyB64);
  15. } else {
  16. this.rawSecret = crypto.getRandomValues(new Uint8Array(16));
  17. }
  18. this.secretKeyPromise = crypto.subtle.importKey(
  19. 'raw',
  20. this.rawSecret,
  21. 'HKDF',
  22. false,
  23. ['deriveKey']
  24. );
  25. this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
  26. return crypto.subtle.deriveKey(
  27. {
  28. name: 'HKDF',
  29. salt: new Uint8Array(),
  30. info: encoder.encode('encryption'),
  31. hash: 'SHA-256'
  32. },
  33. secretKey,
  34. {
  35. name: 'AES-GCM',
  36. length: 128
  37. },
  38. false,
  39. ['encrypt', 'decrypt']
  40. );
  41. });
  42. this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
  43. return crypto.subtle.deriveKey(
  44. {
  45. name: 'HKDF',
  46. salt: new Uint8Array(),
  47. info: encoder.encode('metadata'),
  48. hash: 'SHA-256'
  49. },
  50. secretKey,
  51. {
  52. name: 'AES-GCM',
  53. length: 128
  54. },
  55. false,
  56. ['encrypt', 'decrypt']
  57. );
  58. });
  59. this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
  60. return crypto.subtle.deriveKey(
  61. {
  62. name: 'HKDF',
  63. salt: new Uint8Array(),
  64. info: encoder.encode('authentication'),
  65. hash: 'SHA-256'
  66. },
  67. secretKey,
  68. {
  69. name: 'HMAC',
  70. hash: { name: 'SHA-256' }
  71. },
  72. true,
  73. ['sign']
  74. );
  75. });
  76. }
  77. get nonce() {
  78. return this._nonce;
  79. }
  80. set nonce(n) {
  81. if (n && n !== this._nonce) {
  82. this._nonce = n;
  83. }
  84. }
  85. setIV(ivB64) {
  86. this.iv = b64ToArray(ivB64);
  87. }
  88. setPassword(password, shareUrl) {
  89. this.authKeyPromise = crypto.subtle
  90. .importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
  91. 'deriveKey'
  92. ])
  93. .then(passwordKey =>
  94. crypto.subtle.deriveKey(
  95. {
  96. name: 'PBKDF2',
  97. salt: encoder.encode(shareUrl),
  98. iterations: 100,
  99. hash: 'SHA-256'
  100. },
  101. passwordKey,
  102. {
  103. name: 'HMAC',
  104. hash: 'SHA-256'
  105. },
  106. true,
  107. ['sign']
  108. )
  109. );
  110. }
  111. setAuthKey(authKeyB64) {
  112. this.authKeyPromise = crypto.subtle.importKey(
  113. 'raw',
  114. b64ToArray(authKeyB64),
  115. {
  116. name: 'HMAC',
  117. hash: 'SHA-256'
  118. },
  119. true,
  120. ['sign']
  121. );
  122. }
  123. async authKeyB64() {
  124. const authKey = await this.authKeyPromise;
  125. const rawAuth = await crypto.subtle.exportKey('raw', authKey);
  126. return arrayToB64(new Uint8Array(rawAuth));
  127. }
  128. async authHeader() {
  129. const authKey = await this.authKeyPromise;
  130. const sig = await crypto.subtle.sign(
  131. {
  132. name: 'HMAC'
  133. },
  134. authKey,
  135. b64ToArray(this.nonce)
  136. );
  137. return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
  138. }
  139. async encryptFile(plaintext) {
  140. const encryptKey = await this.encryptKeyPromise;
  141. const ciphertext = await crypto.subtle.encrypt(
  142. {
  143. name: 'AES-GCM',
  144. iv: this.iv,
  145. tagLength: 128
  146. },
  147. encryptKey,
  148. plaintext
  149. );
  150. return ciphertext;
  151. }
  152. async encryptMetadata(metadata) {
  153. const metaKey = await this.metaKeyPromise;
  154. const ciphertext = await crypto.subtle.encrypt(
  155. {
  156. name: 'AES-GCM',
  157. iv: new Uint8Array(12),
  158. tagLength: 128
  159. },
  160. metaKey,
  161. encoder.encode(
  162. JSON.stringify({
  163. iv: arrayToB64(this.iv),
  164. name: metadata.name,
  165. type: metadata.type || 'application/octet-stream'
  166. })
  167. )
  168. );
  169. return ciphertext;
  170. }
  171. encryptStream(plaintext) {
  172. const ece = new ECE(plaintext, this.rawSecret, 'encrypt');
  173. return {
  174. stream: ece.transform(),
  175. streamInfo: ece.info()
  176. };
  177. }
  178. decryptStream(cryptotext) {
  179. const ece = new ECE(cryptotext, this.rawSecret, 'decrypt');
  180. return ece.transform();
  181. }
  182. async decryptFile(ciphertext) {
  183. const encryptKey = await this.encryptKeyPromise;
  184. const plaintext = await crypto.subtle.decrypt(
  185. {
  186. name: 'AES-GCM',
  187. iv: this.iv,
  188. tagLength: 128
  189. },
  190. encryptKey,
  191. ciphertext
  192. );
  193. return plaintext;
  194. }
  195. async decryptMetadata(ciphertext) {
  196. const metaKey = await this.metaKeyPromise;
  197. const plaintext = await crypto.subtle.decrypt(
  198. {
  199. name: 'AES-GCM',
  200. iv: new Uint8Array(12),
  201. tagLength: 128
  202. },
  203. metaKey,
  204. ciphertext
  205. );
  206. return JSON.parse(decoder.decode(plaintext));
  207. }
  208. }