keychain.js 4.1 KB

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