state_machine.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.StateMachine = void 0;
  4. const fs = require("fs/promises");
  5. const net = require("net");
  6. const tls = require("tls");
  7. const bson_1 = require("../bson");
  8. const abstract_cursor_1 = require("../cursor/abstract_cursor");
  9. const deps_1 = require("../deps");
  10. const error_1 = require("../error");
  11. const timeout_1 = require("../timeout");
  12. const utils_1 = require("../utils");
  13. const client_encryption_1 = require("./client_encryption");
  14. const errors_1 = require("./errors");
  15. let socks = null;
  16. function loadSocks() {
  17. if (socks == null) {
  18. const socksImport = (0, deps_1.getSocks)();
  19. if ('kModuleError' in socksImport) {
  20. throw socksImport.kModuleError;
  21. }
  22. socks = socksImport;
  23. }
  24. return socks;
  25. }
  26. // libmongocrypt states
  27. const MONGOCRYPT_CTX_ERROR = 0;
  28. const MONGOCRYPT_CTX_NEED_MONGO_COLLINFO = 1;
  29. const MONGOCRYPT_CTX_NEED_MONGO_MARKINGS = 2;
  30. const MONGOCRYPT_CTX_NEED_MONGO_KEYS = 3;
  31. const MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS = 7;
  32. const MONGOCRYPT_CTX_NEED_KMS = 4;
  33. const MONGOCRYPT_CTX_READY = 5;
  34. const MONGOCRYPT_CTX_DONE = 6;
  35. const HTTPS_PORT = 443;
  36. const stateToString = new Map([
  37. [MONGOCRYPT_CTX_ERROR, 'MONGOCRYPT_CTX_ERROR'],
  38. [MONGOCRYPT_CTX_NEED_MONGO_COLLINFO, 'MONGOCRYPT_CTX_NEED_MONGO_COLLINFO'],
  39. [MONGOCRYPT_CTX_NEED_MONGO_MARKINGS, 'MONGOCRYPT_CTX_NEED_MONGO_MARKINGS'],
  40. [MONGOCRYPT_CTX_NEED_MONGO_KEYS, 'MONGOCRYPT_CTX_NEED_MONGO_KEYS'],
  41. [MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS, 'MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS'],
  42. [MONGOCRYPT_CTX_NEED_KMS, 'MONGOCRYPT_CTX_NEED_KMS'],
  43. [MONGOCRYPT_CTX_READY, 'MONGOCRYPT_CTX_READY'],
  44. [MONGOCRYPT_CTX_DONE, 'MONGOCRYPT_CTX_DONE']
  45. ]);
  46. const INSECURE_TLS_OPTIONS = [
  47. 'tlsInsecure',
  48. 'tlsAllowInvalidCertificates',
  49. 'tlsAllowInvalidHostnames'
  50. ];
  51. /**
  52. * Helper function for logging. Enabled by setting the environment flag MONGODB_CRYPT_DEBUG.
  53. * @param msg - Anything you want to be logged.
  54. */
  55. function debug(msg) {
  56. if (process.env.MONGODB_CRYPT_DEBUG) {
  57. // eslint-disable-next-line no-console
  58. console.error(msg);
  59. }
  60. }
  61. /**
  62. * This is kind of a hack. For `rewrapManyDataKey`, we have tests that
  63. * guarantee that when there are no matching keys, `rewrapManyDataKey` returns
  64. * nothing. We also have tests for auto encryption that guarantee for `encrypt`
  65. * we return an error when there are no matching keys. This error is generated in
  66. * subsequent iterations of the state machine.
  67. * Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
  68. * do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
  69. * will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
  70. * otherwise we'll return `{ v: [] }`.
  71. */
  72. let EMPTY_V;
  73. /**
  74. * @internal
  75. * An internal class that executes across a MongoCryptContext until either
  76. * a finishing state or an error is reached. Do not instantiate directly.
  77. */
  78. // TODO(DRIVERS-2671): clarify CSOT behavior for FLE APIs
  79. class StateMachine {
  80. constructor(options, bsonOptions = (0, bson_1.pluckBSONSerializeOptions)(options)) {
  81. this.options = options;
  82. this.bsonOptions = bsonOptions;
  83. }
  84. /**
  85. * Executes the state machine according to the specification
  86. */
  87. async execute(executor, context, options) {
  88. const keyVaultNamespace = executor._keyVaultNamespace;
  89. const keyVaultClient = executor._keyVaultClient;
  90. const metaDataClient = executor._metaDataClient;
  91. const mongocryptdClient = executor._mongocryptdClient;
  92. const mongocryptdManager = executor._mongocryptdManager;
  93. let result = null;
  94. // Typescript treats getters just like properties: Once you've tested it for equality
  95. // it cannot change. Which is exactly the opposite of what we use state and status for.
  96. // Every call to at least `addMongoOperationResponse` and `finalize` can change the state.
  97. // These wrappers let us write code more naturally and not add compiler exceptions
  98. // to conditions checks inside the state machine.
  99. const getStatus = () => context.status;
  100. const getState = () => context.state;
  101. while (getState() !== MONGOCRYPT_CTX_DONE && getState() !== MONGOCRYPT_CTX_ERROR) {
  102. options.signal?.throwIfAborted();
  103. debug(`[context#${context.id}] ${stateToString.get(getState()) || getState()}`);
  104. switch (getState()) {
  105. case MONGOCRYPT_CTX_NEED_MONGO_COLLINFO: {
  106. const filter = (0, bson_1.deserialize)(context.nextMongoOperation());
  107. if (!metaDataClient) {
  108. throw new errors_1.MongoCryptError('unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_COLLINFO but metadata client is undefined');
  109. }
  110. const collInfoCursor = this.fetchCollectionInfo(metaDataClient, context.ns, filter, options);
  111. for await (const collInfo of collInfoCursor) {
  112. context.addMongoOperationResponse((0, bson_1.serialize)(collInfo));
  113. if (getState() === MONGOCRYPT_CTX_ERROR)
  114. break;
  115. }
  116. if (getState() === MONGOCRYPT_CTX_ERROR)
  117. break;
  118. context.finishMongoOperation();
  119. break;
  120. }
  121. case MONGOCRYPT_CTX_NEED_MONGO_MARKINGS: {
  122. const command = context.nextMongoOperation();
  123. if (getState() === MONGOCRYPT_CTX_ERROR)
  124. break;
  125. if (!mongocryptdClient) {
  126. throw new errors_1.MongoCryptError('unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_MARKINGS but mongocryptdClient is undefined');
  127. }
  128. // When we are using the shared library, we don't have a mongocryptd manager.
  129. const markedCommand = mongocryptdManager
  130. ? await mongocryptdManager.withRespawn(this.markCommand.bind(this, mongocryptdClient, context.ns, command, options))
  131. : await this.markCommand(mongocryptdClient, context.ns, command, options);
  132. context.addMongoOperationResponse(markedCommand);
  133. context.finishMongoOperation();
  134. break;
  135. }
  136. case MONGOCRYPT_CTX_NEED_MONGO_KEYS: {
  137. const filter = context.nextMongoOperation();
  138. const keys = await this.fetchKeys(keyVaultClient, keyVaultNamespace, filter, options);
  139. if (keys.length === 0) {
  140. // See docs on EMPTY_V
  141. result = EMPTY_V ??= (0, bson_1.serialize)({ v: [] });
  142. }
  143. for (const key of keys) {
  144. context.addMongoOperationResponse((0, bson_1.serialize)(key));
  145. }
  146. context.finishMongoOperation();
  147. break;
  148. }
  149. case MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS: {
  150. const kmsProviders = await executor.askForKMSCredentials();
  151. context.provideKMSProviders((0, bson_1.serialize)(kmsProviders));
  152. break;
  153. }
  154. case MONGOCRYPT_CTX_NEED_KMS: {
  155. await Promise.all(this.requests(context, options));
  156. context.finishKMSRequests();
  157. break;
  158. }
  159. case MONGOCRYPT_CTX_READY: {
  160. const finalizedContext = context.finalize();
  161. if (getState() === MONGOCRYPT_CTX_ERROR) {
  162. const message = getStatus().message || 'Finalization error';
  163. throw new errors_1.MongoCryptError(message);
  164. }
  165. result = finalizedContext;
  166. break;
  167. }
  168. default:
  169. throw new errors_1.MongoCryptError(`Unknown state: ${getState()}`);
  170. }
  171. }
  172. if (getState() === MONGOCRYPT_CTX_ERROR || result == null) {
  173. const message = getStatus().message;
  174. if (!message) {
  175. debug(`unidentifiable error in MongoCrypt - received an error status from \`libmongocrypt\` but received no error message.`);
  176. }
  177. throw new errors_1.MongoCryptError(message ??
  178. 'unidentifiable error in MongoCrypt - received an error status from `libmongocrypt` but received no error message.');
  179. }
  180. return result;
  181. }
  182. /**
  183. * Handles the request to the KMS service. Exposed for testing purposes. Do not directly invoke.
  184. * @param kmsContext - A C++ KMS context returned from the bindings
  185. * @returns A promise that resolves when the KMS reply has be fully parsed
  186. */
  187. async kmsRequest(request, options) {
  188. const parsedUrl = request.endpoint.split(':');
  189. const port = parsedUrl[1] != null ? Number.parseInt(parsedUrl[1], 10) : HTTPS_PORT;
  190. const socketOptions = {
  191. host: parsedUrl[0],
  192. servername: parsedUrl[0],
  193. port,
  194. ...(0, client_encryption_1.autoSelectSocketOptions)(this.options.socketOptions || {})
  195. };
  196. const message = request.message;
  197. const buffer = new utils_1.BufferPool();
  198. let netSocket;
  199. let socket;
  200. function destroySockets() {
  201. for (const sock of [socket, netSocket]) {
  202. if (sock) {
  203. sock.destroy();
  204. }
  205. }
  206. }
  207. function onerror(cause) {
  208. return new errors_1.MongoCryptError('KMS request failed', { cause });
  209. }
  210. function onclose() {
  211. return new errors_1.MongoCryptError('KMS request closed');
  212. }
  213. const tlsOptions = this.options.tlsOptions;
  214. if (tlsOptions) {
  215. const kmsProvider = request.kmsProvider;
  216. const providerTlsOptions = tlsOptions[kmsProvider];
  217. if (providerTlsOptions) {
  218. const error = this.validateTlsOptions(kmsProvider, providerTlsOptions);
  219. if (error) {
  220. throw error;
  221. }
  222. try {
  223. await this.setTlsOptions(providerTlsOptions, socketOptions);
  224. }
  225. catch (err) {
  226. throw onerror(err);
  227. }
  228. }
  229. }
  230. let abortListener;
  231. try {
  232. if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) {
  233. netSocket = new net.Socket();
  234. const { promise: willConnect, reject: rejectOnNetSocketError, resolve: resolveOnNetSocketConnect } = (0, utils_1.promiseWithResolvers)();
  235. netSocket
  236. .once('error', err => rejectOnNetSocketError(onerror(err)))
  237. .once('close', () => rejectOnNetSocketError(onclose()))
  238. .once('connect', () => resolveOnNetSocketConnect());
  239. const netSocketOptions = {
  240. ...socketOptions,
  241. host: this.options.proxyOptions.proxyHost,
  242. port: this.options.proxyOptions.proxyPort || 1080
  243. };
  244. netSocket.connect(netSocketOptions);
  245. await willConnect;
  246. try {
  247. socks ??= loadSocks();
  248. socketOptions.socket = (await socks.SocksClient.createConnection({
  249. existing_socket: netSocket,
  250. command: 'connect',
  251. destination: { host: socketOptions.host, port: socketOptions.port },
  252. proxy: {
  253. // host and port are ignored because we pass existing_socket
  254. host: 'iLoveJavaScript',
  255. port: 0,
  256. type: 5,
  257. userId: this.options.proxyOptions.proxyUsername,
  258. password: this.options.proxyOptions.proxyPassword
  259. }
  260. })).socket;
  261. }
  262. catch (err) {
  263. throw onerror(err);
  264. }
  265. }
  266. socket = tls.connect(socketOptions, () => {
  267. socket.write(message);
  268. });
  269. const { promise: willResolveKmsRequest, reject: rejectOnTlsSocketError, resolve } = (0, utils_1.promiseWithResolvers)();
  270. abortListener = (0, utils_1.addAbortListener)(options?.signal, function () {
  271. destroySockets();
  272. rejectOnTlsSocketError(this.reason);
  273. });
  274. socket
  275. .once('error', err => rejectOnTlsSocketError(onerror(err)))
  276. .once('close', () => rejectOnTlsSocketError(onclose()))
  277. .on('data', data => {
  278. buffer.append(data);
  279. while (request.bytesNeeded > 0 && buffer.length) {
  280. const bytesNeeded = Math.min(request.bytesNeeded, buffer.length);
  281. request.addResponse(buffer.read(bytesNeeded));
  282. }
  283. if (request.bytesNeeded <= 0) {
  284. resolve();
  285. }
  286. });
  287. await (options?.timeoutContext?.csotEnabled()
  288. ? Promise.all([
  289. willResolveKmsRequest,
  290. timeout_1.Timeout.expires(options.timeoutContext?.remainingTimeMS)
  291. ])
  292. : willResolveKmsRequest);
  293. }
  294. catch (error) {
  295. if (error instanceof timeout_1.TimeoutError)
  296. throw new error_1.MongoOperationTimeoutError('KMS request timed out');
  297. throw error;
  298. }
  299. finally {
  300. // There's no need for any more activity on this socket at this point.
  301. destroySockets();
  302. abortListener?.[utils_1.kDispose]();
  303. }
  304. }
  305. *requests(context, options) {
  306. for (let request = context.nextKMSRequest(); request != null; request = context.nextKMSRequest()) {
  307. yield this.kmsRequest(request, options);
  308. }
  309. }
  310. /**
  311. * Validates the provided TLS options are secure.
  312. *
  313. * @param kmsProvider - The KMS provider name.
  314. * @param tlsOptions - The client TLS options for the provider.
  315. *
  316. * @returns An error if any option is invalid.
  317. */
  318. validateTlsOptions(kmsProvider, tlsOptions) {
  319. const tlsOptionNames = Object.keys(tlsOptions);
  320. for (const option of INSECURE_TLS_OPTIONS) {
  321. if (tlsOptionNames.includes(option)) {
  322. return new errors_1.MongoCryptError(`Insecure TLS options prohibited for ${kmsProvider}: ${option}`);
  323. }
  324. }
  325. }
  326. /**
  327. * Sets only the valid secure TLS options.
  328. *
  329. * @param tlsOptions - The client TLS options for the provider.
  330. * @param options - The existing connection options.
  331. */
  332. async setTlsOptions(tlsOptions, options) {
  333. // If a secureContext is provided, ensure it is set.
  334. if (tlsOptions.secureContext) {
  335. options.secureContext = tlsOptions.secureContext;
  336. }
  337. if (tlsOptions.tlsCertificateKeyFile) {
  338. const cert = await fs.readFile(tlsOptions.tlsCertificateKeyFile);
  339. options.cert = options.key = cert;
  340. }
  341. if (tlsOptions.tlsCAFile) {
  342. options.ca = await fs.readFile(tlsOptions.tlsCAFile);
  343. }
  344. if (tlsOptions.tlsCertificateKeyFilePassword) {
  345. options.passphrase = tlsOptions.tlsCertificateKeyFilePassword;
  346. }
  347. }
  348. /**
  349. * Fetches collection info for a provided namespace, when libmongocrypt
  350. * enters the `MONGOCRYPT_CTX_NEED_MONGO_COLLINFO` state. The result is
  351. * used to inform libmongocrypt of the schema associated with this
  352. * namespace. Exposed for testing purposes. Do not directly invoke.
  353. *
  354. * @param client - A MongoClient connected to the topology
  355. * @param ns - The namespace to list collections from
  356. * @param filter - A filter for the listCollections command
  357. * @param callback - Invoked with the info of the requested collection, or with an error
  358. */
  359. fetchCollectionInfo(client, ns, filter, options) {
  360. const { db } = utils_1.MongoDBCollectionNamespace.fromString(ns);
  361. const cursor = client.db(db).listCollections(filter, {
  362. promoteLongs: false,
  363. promoteValues: false,
  364. timeoutContext: options?.timeoutContext && new abstract_cursor_1.CursorTimeoutContext(options?.timeoutContext, Symbol()),
  365. signal: options?.signal,
  366. nameOnly: false
  367. });
  368. return cursor;
  369. }
  370. /**
  371. * Calls to the mongocryptd to provide markings for a command.
  372. * Exposed for testing purposes. Do not directly invoke.
  373. * @param client - A MongoClient connected to a mongocryptd
  374. * @param ns - The namespace (database.collection) the command is being executed on
  375. * @param command - The command to execute.
  376. * @param callback - Invoked with the serialized and marked bson command, or with an error
  377. */
  378. async markCommand(client, ns, command, options) {
  379. const { db } = utils_1.MongoDBCollectionNamespace.fromString(ns);
  380. const bsonOptions = { promoteLongs: false, promoteValues: false };
  381. const rawCommand = (0, bson_1.deserialize)(command, bsonOptions);
  382. const commandOptions = {
  383. timeoutMS: undefined,
  384. signal: undefined
  385. };
  386. if (options?.timeoutContext?.csotEnabled()) {
  387. commandOptions.timeoutMS = options.timeoutContext.remainingTimeMS;
  388. }
  389. if (options?.signal) {
  390. commandOptions.signal = options.signal;
  391. }
  392. const response = await client.db(db).command(rawCommand, {
  393. ...bsonOptions,
  394. ...commandOptions
  395. });
  396. return (0, bson_1.serialize)(response, this.bsonOptions);
  397. }
  398. /**
  399. * Requests keys from the keyVault collection on the topology.
  400. * Exposed for testing purposes. Do not directly invoke.
  401. * @param client - A MongoClient connected to the topology
  402. * @param keyVaultNamespace - The namespace (database.collection) of the keyVault Collection
  403. * @param filter - The filter for the find query against the keyVault Collection
  404. * @param callback - Invoked with the found keys, or with an error
  405. */
  406. fetchKeys(client, keyVaultNamespace, filter, options) {
  407. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(keyVaultNamespace);
  408. const commandOptions = {
  409. timeoutContext: undefined,
  410. signal: undefined
  411. };
  412. if (options?.timeoutContext != null) {
  413. commandOptions.timeoutContext = new abstract_cursor_1.CursorTimeoutContext(options.timeoutContext, Symbol());
  414. }
  415. if (options?.signal != null) {
  416. commandOptions.signal = options.signal;
  417. }
  418. return client
  419. .db(dbName)
  420. .collection(collectionName, { readConcern: { level: 'majority' } })
  421. .find((0, bson_1.deserialize)(filter), commandOptions)
  422. .toArray();
  423. }
  424. }
  425. exports.StateMachine = StateMachine;
  426. //# sourceMappingURL=state_machine.js.map