Просмотр исходного кода

Merge pull request #21623 from overleaf/jpa-s3-ssec-static-dek-path

[object-persistor] s3SSEC: use static path for DEK inside project folder

GitOrigin-RevId: 0c43ef8964c16d3e7638f17ff47b1c4641e439df
Jakob Ackermann 1 год назад
Родитель
Сommit
d5478c11ea

+ 45 - 36
libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js

@@ -13,6 +13,7 @@ const {
   ReadError,
 } = require('./Errors')
 const logger = require('@overleaf/logger')
+const Path = require('path')
 
 const generateKey = promisify(Crypto.generateKey)
 const hkdf = promisify(Crypto.hkdf)
@@ -26,8 +27,8 @@ const AES256_KEY_LENGTH = 32
 /**
  * @typedef {Object} Settings
  * @property {boolean} automaticallyRotateDEKEncryption
+ * @property {string} dataEncryptionKeyBucketName
  * @property {boolean} ignoreErrorsFromDEKReEncryption
- * @property {(bucketName: string, path: string) => {bucketName: string, path: string}} pathToDataEncryptionKeyPath
  * @property {(bucketName: string, path: string) => string} pathToProjectFolder
  * @property {() => Promise<Array<RootKeyEncryptionKey>>} getRootKeyEncryptionKeys
  */
@@ -102,9 +103,12 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
    * @param {Settings} settings
    */
   constructor(settings) {
+    if (!settings.dataEncryptionKeyBucketName) {
+      throw new Error('settings.dataEncryptionKeyBucketName is missing')
+    }
     super(settings)
     this.#settings = settings
-    this.#availableKeyEncryptionKeysPromise = this.#settings
+    this.#availableKeyEncryptionKeysPromise = settings
       .getRootKeyEncryptionKeys()
       .then(rootKEKs => {
         if (rootKEKs.length === 0) throw new Error('no root kek provided')
@@ -119,13 +123,21 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
   /**
    * @param {string} bucketName
    * @param {string} path
+   * @return {{dekPath: string, projectFolder: string}}
+   */
+  #buildProjectPaths(bucketName, path) {
+    const projectFolder = this.#settings.pathToProjectFolder(bucketName, path)
+    const dekPath = Path.join(projectFolder, 'dek')
+    return { projectFolder, dekPath }
+  }
+
+  /**
+   * @param {string} projectFolder
    * @return {Promise<SSECOptions>}
    */
-  async #getCurrentKeyEncryptionKey(bucketName, path) {
+  async #getCurrentKeyEncryptionKey(projectFolder) {
     const [currentRootKEK] = await this.#availableKeyEncryptionKeysPromise
-    return await currentRootKEK.forProject(
-      this.#settings.pathToProjectFolder(bucketName, path)
-    )
+    return await currentRootKEK.forProject(projectFolder)
   }
 
   /**
@@ -133,15 +145,15 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
    * @param {string} path
    */
   async getDataEncryptionKeySize(bucketName, path) {
-    const dekPath = this.#settings.pathToDataEncryptionKeyPath(bucketName, path)
+    const { projectFolder, dekPath } = this.#buildProjectPaths(bucketName, path)
     for (const rootKEK of await this.#availableKeyEncryptionKeysPromise) {
-      const ssecOptions = await rootKEK.forProject(
-        this.#settings.pathToProjectFolder(bucketName, path)
-      )
+      const ssecOptions = await rootKEK.forProject(projectFolder)
       try {
-        return await super.getObjectSize(dekPath.bucketName, dekPath.path, {
-          ssecOptions,
-        })
+        return await super.getObjectSize(
+          this.#settings.dataEncryptionKeyBucketName,
+          dekPath,
+          { ssecOptions }
+        )
       } catch (err) {
         if (isForbiddenError(err)) continue
         throw err
@@ -180,15 +192,15 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
     const dataEncryptionKey = (
       await generateKey('aes', { length: 256 })
     ).export()
-    const dekPath = this.#settings.pathToDataEncryptionKeyPath(bucketName, path)
+    const { projectFolder, dekPath } = this.#buildProjectPaths(bucketName, path)
     await super.sendStream(
-      dekPath.bucketName,
-      dekPath.path,
+      this.#settings.dataEncryptionKeyBucketName,
+      dekPath,
       Stream.Readable.from([dataEncryptionKey]),
       {
         // Do not overwrite any objects if already created
         ifNoneMatch: '*',
-        ssecOptions: await this.#getCurrentKeyEncryptionKey(bucketName, path),
+        ssecOptions: await this.#getCurrentKeyEncryptionKey(projectFolder),
       }
     )
     return new SSECOptions(dataEncryptionKey)
@@ -200,17 +212,17 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
    * @return {Promise<SSECOptions>}
    */
   async #getExistingDataEncryptionKeyOptions(bucketName, path) {
-    const dekPath = this.#settings.pathToDataEncryptionKeyPath(bucketName, path)
+    const { projectFolder, dekPath } = this.#buildProjectPaths(bucketName, path)
     let res
     let kekIndex = 0
     for (const rootKEK of await this.#availableKeyEncryptionKeysPromise) {
-      const ssecOptions = await rootKEK.forProject(
-        this.#settings.pathToProjectFolder(bucketName, path)
-      )
+      const ssecOptions = await rootKEK.forProject(projectFolder)
       try {
-        res = await super.getObjectStream(dekPath.bucketName, dekPath.path, {
-          ssecOptions,
-        })
+        res = await super.getObjectStream(
+          this.#settings.dataEncryptionKeyBucketName,
+          dekPath,
+          { ssecOptions }
+        )
       } catch (err) {
         if (isForbiddenError(err)) {
           kekIndex++
@@ -224,20 +236,17 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
     await Stream.promises.pipeline(res, buf)
 
     if (kekIndex !== 0 && this.#settings.automaticallyRotateDEKEncryption) {
-      const ssecOptions = await this.#getCurrentKeyEncryptionKey(
-        bucketName,
-        path
-      )
+      const ssecOptions = await this.#getCurrentKeyEncryptionKey(projectFolder)
       try {
         await super.sendStream(
-          dekPath.bucketName,
-          dekPath.path,
+          this.#settings.dataEncryptionKeyBucketName,
+          dekPath,
           Stream.Readable.from([buf.getContents()]),
           { ssecOptions }
         )
       } catch (err) {
         if (this.#settings.ignoreErrorsFromDEKReEncryption) {
-          logger.warn({ err, ...dekPath }, 'failed to persist re-encrypted DEK')
+          logger.warn({ err, dekPath }, 'failed to persist re-encrypted DEK')
         } else {
           throw err
         }
@@ -309,12 +318,12 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
   async deleteDirectory(bucketName, path, continuationToken) {
     // Note: Listing/Deleting a prefix does not require SSE-C credentials.
     await super.deleteDirectory(bucketName, path, continuationToken)
-    if (this.#settings.pathToProjectFolder(bucketName, path) === path) {
-      const dekPath = this.#settings.pathToDataEncryptionKeyPath(
-        bucketName,
-        path
+    const { projectFolder, dekPath } = this.#buildProjectPaths(bucketName, path)
+    if (projectFolder === path) {
+      await super.deleteObject(
+        this.#settings.dataEncryptionKeyBucketName,
+        dekPath
       )
-      await super.deleteObject(dekPath.bucketName, dekPath.path)
     }
   }
 

+ 1 - 7
services/filestore/test/acceptance/js/TestConfig.js

@@ -41,17 +41,11 @@ function s3SSECConfig() {
     ...s3Config(),
     ignoreErrorsFromDEKReEncryption: false,
     automaticallyRotateDEKEncryption: true,
+    dataEncryptionKeyBucketName: process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME,
     pathToProjectFolder(_bucketName, path) {
       const [projectFolder] = path.match(/^[a-f0-9]+\//)
       return projectFolder
     },
-    pathToDataEncryptionKeyPath(_bucketName, path) {
-      const [projectFolder] = path.match(/^[a-f0-9]+\//)
-      return {
-        bucketName: process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME,
-        path: Path.join(projectFolder, 'dek'),
-      }
-    },
     async getRootKeyEncryptionKeys() {
       return S3SSECKeys
     },