fxa.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. /* global AUTH_CONFIG */
  2. import { arrayToB64, b64ToArray } from './utils';
  3. const encoder = new TextEncoder();
  4. const decoder = new TextDecoder();
  5. function getOtherInfo(enc) {
  6. const name = encoder.encode(enc);
  7. const length = 256;
  8. const buffer = new ArrayBuffer(name.length + 16);
  9. const dv = new DataView(buffer);
  10. const result = new Uint8Array(buffer);
  11. let i = 0;
  12. dv.setUint32(i, name.length);
  13. i += 4;
  14. result.set(name, i);
  15. i += name.length;
  16. dv.setUint32(i, 0);
  17. i += 4;
  18. dv.setUint32(i, 0);
  19. i += 4;
  20. dv.setUint32(i, length);
  21. return result;
  22. }
  23. function concat(b1, b2) {
  24. const result = new Uint8Array(b1.length + b2.length);
  25. result.set(b1, 0);
  26. result.set(b2, b1.length);
  27. return result;
  28. }
  29. async function concatKdf(key, enc) {
  30. if (key.length !== 32) {
  31. throw new Error('unsupported key length');
  32. }
  33. const otherInfo = getOtherInfo(enc);
  34. const buffer = new ArrayBuffer(4 + key.length + otherInfo.length);
  35. const dv = new DataView(buffer);
  36. const concat = new Uint8Array(buffer);
  37. dv.setUint32(0, 1);
  38. concat.set(key, 4);
  39. concat.set(otherInfo, key.length + 4);
  40. const result = await crypto.subtle.digest('SHA-256', concat);
  41. return new Uint8Array(result);
  42. }
  43. export async function prepareScopedBundleKey(storage) {
  44. const keys = await crypto.subtle.generateKey(
  45. {
  46. name: 'ECDH',
  47. namedCurve: 'P-256'
  48. },
  49. true,
  50. ['deriveBits']
  51. );
  52. const privateJwk = await crypto.subtle.exportKey('jwk', keys.privateKey);
  53. const publicJwk = await crypto.subtle.exportKey('jwk', keys.publicKey);
  54. const kid = await crypto.subtle.digest(
  55. 'SHA-256',
  56. encoder.encode(JSON.stringify(publicJwk))
  57. );
  58. privateJwk.kid = kid;
  59. publicJwk.kid = kid;
  60. storage.set('scopedBundlePrivateKey', JSON.stringify(privateJwk));
  61. return arrayToB64(encoder.encode(JSON.stringify(publicJwk)));
  62. }
  63. export async function decryptBundle(storage, bundle) {
  64. const privateJwk = JSON.parse(storage.get('scopedBundlePrivateKey'));
  65. storage.remove('scopedBundlePrivateKey');
  66. const privateKey = await crypto.subtle.importKey(
  67. 'jwk',
  68. privateJwk,
  69. {
  70. name: 'ECDH',
  71. namedCurve: 'P-256'
  72. },
  73. false,
  74. ['deriveBits']
  75. );
  76. const jweParts = bundle.split('.');
  77. if (jweParts.length !== 5) {
  78. throw new Error('invalid jwe');
  79. }
  80. const header = JSON.parse(decoder.decode(b64ToArray(jweParts[0])));
  81. const additionalData = encoder.encode(jweParts[0]);
  82. const iv = b64ToArray(jweParts[2]);
  83. const ciphertext = b64ToArray(jweParts[3]);
  84. const tag = b64ToArray(jweParts[4]);
  85. if (header.alg !== 'ECDH-ES' || header.enc !== 'A256GCM') {
  86. throw new Error('unsupported jwe type');
  87. }
  88. const publicKey = await crypto.subtle.importKey(
  89. 'jwk',
  90. header.epk,
  91. {
  92. name: 'ECDH',
  93. namedCurve: 'P-256'
  94. },
  95. false,
  96. []
  97. );
  98. const sharedBits = await crypto.subtle.deriveBits(
  99. {
  100. name: 'ECDH',
  101. public: publicKey
  102. },
  103. privateKey,
  104. 256
  105. );
  106. const rawSharedKey = await concatKdf(new Uint8Array(sharedBits), header.enc);
  107. const sharedKey = await crypto.subtle.importKey(
  108. 'raw',
  109. rawSharedKey,
  110. {
  111. name: 'AES-GCM'
  112. },
  113. false,
  114. ['decrypt']
  115. );
  116. const plaintext = await crypto.subtle.decrypt(
  117. {
  118. name: 'AES-GCM',
  119. iv: iv,
  120. additionalData: additionalData,
  121. tagLength: tag.length * 8
  122. },
  123. sharedKey,
  124. concat(ciphertext, tag)
  125. );
  126. return JSON.parse(decoder.decode(plaintext));
  127. }
  128. export async function preparePkce(storage) {
  129. const verifier = arrayToB64(crypto.getRandomValues(new Uint8Array(64)));
  130. storage.set('pkceVerifier', verifier);
  131. const challenge = await crypto.subtle.digest(
  132. 'SHA-256',
  133. encoder.encode(verifier)
  134. );
  135. return arrayToB64(new Uint8Array(challenge));
  136. }
  137. export async function deriveFileListKey(ikm) {
  138. const baseKey = await crypto.subtle.importKey(
  139. 'raw',
  140. b64ToArray(ikm),
  141. { name: 'HKDF' },
  142. false,
  143. ['deriveKey']
  144. );
  145. const fileListKey = await crypto.subtle.deriveKey(
  146. {
  147. name: 'HKDF',
  148. salt: new Uint8Array(),
  149. info: encoder.encode('fileList'),
  150. hash: 'SHA-256'
  151. },
  152. baseKey,
  153. {
  154. name: 'AES-GCM',
  155. length: 128
  156. },
  157. true,
  158. ['encrypt', 'decrypt']
  159. );
  160. const rawFileListKey = await crypto.subtle.exportKey('raw', fileListKey);
  161. return arrayToB64(new Uint8Array(rawFileListKey));
  162. }
  163. export async function getFileListKey(storage, bundle) {
  164. const jwks = await decryptBundle(storage, bundle);
  165. const jwk = jwks[AUTH_CONFIG.key_scope];
  166. return deriveFileListKey(jwk.k);
  167. }