fxa.js 4.3 KB

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