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
This commit is contained in:
Jakob Ackermann 2024-11-08 09:32:24 +01:00 committed by Copybot
parent a67560c26b
commit d5478c11ea
2 changed files with 46 additions and 43 deletions

View file

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

View file

@ -41,17 +41,11 @@ function s3SSECConfig() {
...s3Config(), ...s3Config(),
ignoreErrorsFromDEKReEncryption: false, ignoreErrorsFromDEKReEncryption: false,
automaticallyRotateDEKEncryption: true, automaticallyRotateDEKEncryption: true,
dataEncryptionKeyBucketName: process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME,
pathToProjectFolder(_bucketName, path) { pathToProjectFolder(_bucketName, path) {
const [projectFolder] = path.match(/^[a-f0-9]+\//) const [projectFolder] = path.match(/^[a-f0-9]+\//)
return projectFolder 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() { async getRootKeyEncryptionKeys() {
return S3SSECKeys return S3SSECKeys
}, },