Skip to content

Commit

Permalink
Add support for the encryption key rotation to the encrypted saved ob…
Browse files Browse the repository at this point in the history
…jects plugin.
  • Loading branch information
azasypkin committed Aug 5, 2020
1 parent 7c60b09 commit a0a1344
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 39 deletions.
38 changes: 28 additions & 10 deletions x-pack/plugins/encrypted_saved_objects/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,34 @@
import crypto from 'crypto';
import { map } from 'rxjs/operators';
import { schema, TypeOf } from '@kbn/config-schema';
import { UnwrapObservable } from '@kbn/utility-types';
import { PluginInitializerContext } from 'src/core/server';

export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
encryptionKey: schema.conditional(
schema.contextRef('dist'),
true,
schema.maybe(schema.string({ minLength: 32 })),
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
),
});
export type ConfigType = UnwrapObservable<ReturnType<typeof createConfig$>>;

export const ConfigSchema = schema.object(
{
enabled: schema.boolean({ defaultValue: true }),
encryptionKey: schema.conditional(
schema.contextRef('dist'),
true,
schema.maybe(schema.string({ minLength: 32 })),
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
),
keyRotation: schema.object({
decryptionOnlyKeys: schema.arrayOf(schema.string({ minLength: 32 }), { defaultValue: [] }),
batchSize: schema.number({ min: 1, defaultValue: 100 }),
}),
},
{
validate(value) {
const decryptionOnlyKeys = value.keyRotation?.decryptionOnlyKeys ?? [];
if (value.encryptionKey && decryptionOnlyKeys.includes(value.encryptionKey)) {
return '`keyRotation.decryptionOnlyKeys` cannot contain primary encryption key specified in `encryptionKey`.';
}
},
}
);

export function createConfig$(context: PluginInitializerContext) {
return context.config.create<TypeOf<typeof ConfigSchema>>().pipe(
Expand All @@ -37,7 +54,8 @@ export function createConfig$(context: PluginInitializerContext) {
}

return {
config: { ...config, encryptionKey },
...config,
encryptionKey,
usingEphemeralEncryptionKey,
};
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ interface CommonParameters {
user?: AuthenticatedUser;
}

/**
* Describes parameters for the decrypt methods.
*/
interface DecryptParameters extends CommonParameters {
/**
* Indicates whether decryption should only be performed using secondary decryption-only keys.
*/
useDecryptionOnlyKeys?: boolean;
}

/**
* Utility function that gives array representation of the saved object descriptor respecting
* optional `namespace` property.
Expand Down Expand Up @@ -80,15 +90,21 @@ export class EncryptedSavedObjectsService {
> = new Map();

/**
* @param crypto nodeCrypto instance.
* @param cryptos nodeCrypto instances used for encryption and/or decryption. The first crypto
* in the list is considered as primary and it's the only one that is used for both encryption and
* decryption, the rest are decryption-only cryptos.
* @param logger Ordinary logger instance.
* @param audit Audit logger instance.
*/
constructor(
private readonly crypto: Readonly<Crypto>,
private readonly cryptos: Readonly<Crypto[]>,
private readonly logger: Logger,
private readonly audit: EncryptedSavedObjectsAuditLogger
) {}
) {
if (cryptos.length === 0 || !cryptos[0]) {
throw new Error('The list of cryptos should include at least one item.');
}
}

/**
* Registers saved object type as the one that contains attributes that should be encrypted.
Expand Down Expand Up @@ -136,7 +152,7 @@ export class EncryptedSavedObjectsService {
descriptor: SavedObjectDescriptor,
attributes: T,
originalAttributes?: T,
params?: CommonParameters
params?: DecryptParameters
) {
const typeDefinition = this.typeDefinitions.get(descriptor.type);
if (typeDefinition === undefined) {
Expand Down Expand Up @@ -174,7 +190,7 @@ export class EncryptedSavedObjectsService {
Object.fromEntries(
Object.entries(attributes).filter(([key]) => !typeDefinition.shouldBeStripped(key))
) as T,
{ user: params?.user }
params
);
} catch (err) {
decryptionError = err;
Expand Down Expand Up @@ -261,13 +277,14 @@ export class EncryptedSavedObjectsService {
attributes: T,
params?: CommonParameters
): Promise<T> {
const encrypter = this.getEncrypter();
const iterator = this.attributesToEncryptIterator<T>(descriptor, attributes, params);

let iteratorResult = iterator.next();
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
try {
iteratorResult = iterator.next(await this.crypto.encrypt(attributeValue, encryptionAAD));
iteratorResult = iterator.next(await encrypter.encrypt(attributeValue, encryptionAAD));
} catch (err) {
iterator.throw!(err);
}
Expand All @@ -290,13 +307,14 @@ export class EncryptedSavedObjectsService {
attributes: T,
params?: CommonParameters
): T {
const encrypter = this.getEncrypter();
const iterator = this.attributesToEncryptIterator<T>(descriptor, attributes, params);

let iteratorResult = iterator.next();
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
try {
iteratorResult = iterator.next(this.crypto.encryptSync(attributeValue, encryptionAAD));
iteratorResult = iterator.next(encrypter.encryptSync(attributeValue, encryptionAAD));
} catch (err) {
iterator.throw!(err);
}
Expand All @@ -318,19 +336,31 @@ export class EncryptedSavedObjectsService {
public async decryptAttributes<T extends Record<string, unknown>>(
descriptor: SavedObjectDescriptor,
attributes: T,
params?: CommonParameters
params?: DecryptParameters
): Promise<T> {
const decrypters = this.getDecrypters(params?.useDecryptionOnlyKeys);
const iterator = this.attributesToDecryptIterator<T>(descriptor, attributes, params);

let iteratorResult = iterator.next();
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
try {
iteratorResult = iterator.next(
(await this.crypto.decrypt(attributeValue, encryptionAAD)) as string
);
} catch (err) {
iterator.throw!(err);

let decryptionError;
for (const decrypter of decrypters) {
try {
iteratorResult = iterator.next(await decrypter.decrypt(attributeValue, encryptionAAD));
decryptionError = undefined;
break;
} catch (err) {
// Remember the error thrown when we tried to decrypt with the primary key.
if (!decryptionError) {
decryptionError = err;
}
}
}

if (decryptionError) {
iterator.throw!(decryptionError);
}
}

Expand All @@ -350,17 +380,31 @@ export class EncryptedSavedObjectsService {
public decryptAttributesSync<T extends Record<string, unknown>>(
descriptor: SavedObjectDescriptor,
attributes: T,
params?: CommonParameters
params?: DecryptParameters
): T {
const decrypters = this.getDecrypters(params?.useDecryptionOnlyKeys);
const iterator = this.attributesToDecryptIterator<T>(descriptor, attributes, params);

let iteratorResult = iterator.next();
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
try {
iteratorResult = iterator.next(this.crypto.decryptSync(attributeValue, encryptionAAD));
} catch (err) {
iterator.throw!(err);

let decryptionError;
for (const decrypter of decrypters) {
try {
iteratorResult = iterator.next(decrypter.decryptSync(attributeValue, encryptionAAD));
decryptionError = undefined;
break;
} catch (err) {
// Remember the error thrown when we tried to decrypt with the primary key.
if (!decryptionError) {
decryptionError = err;
}
}
}

if (decryptionError) {
iterator.throw!(decryptionError);
}
}

Expand Down Expand Up @@ -464,4 +508,30 @@ export class EncryptedSavedObjectsService {

return stringify([...descriptorToArray(descriptor), attributesAAD]);
}

/**
* Returns NodeCrypto instance used for encryption (the primary crypto).
*/
private getEncrypter() {
return this.cryptos[0];
}

/**
* Returns list of NodeCrypto instances used for decryption.
* @param useDecryptionOnlyKeys Specifies whether returned decrypters should include only those
* that are using decryption only keys (the secondary cryptos).
*/
private getDecrypters(useDecryptionOnlyKeys?: boolean) {
if (!useDecryptionOnlyKeys) {
return this.cryptos;
}

if (this.cryptos.length === 1) {
throw new Error(
`"useDecryptionOnlyKeys" cannot be set when decryption only keys aren't configured.`
);
}

return this.cryptos.slice(1);
}
}
Loading

0 comments on commit a0a1344

Please sign in to comment.