mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05: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,
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue