mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-22 21:43:47 +00:00
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:
parent
a67560c26b
commit
d5478c11ea
2 changed files with 46 additions and 43 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue