Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions modules/cache-material/src/cache_clock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

/* The time source for a cache's entries, and for the caching CMMs that check their age.
* A cache built with getLocalCryptographicMaterialsCache uses Date.now.
* The ESDK test server builds caches with a clock it can move forward,
* so tests can expire entries without waiting.
* This module is not exported from the package index.
*/
import { CryptographicMaterialsCache } from './cryptographic_materials_cache'

export type Clock = () => number

const clocks = new WeakMap<CryptographicMaterialsCache<any>, Clock>()

export function setClock(
cache: CryptographicMaterialsCache<any>,
clock: Clock
): void {
clocks.set(cache, clock)
}

/* A cache from another implementation has no registered clock, so it gets Date.now. */
export function clockFor(cache: CryptographicMaterialsCache<any>): Clock {
return clocks.get(cache) || Date.now
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Entry,
} from './cryptographic_materials_cache'
import { CryptographicMaterialsCacheKeyHelpersInterface } from './build_cryptographic_materials_cache_key_helpers'
import { clockFor } from './cache_clock'

export function decorateProperties<S extends SupportedAlgorithmSuites>(
obj: CachingMaterialsManager<S>,
Expand Down Expand Up @@ -126,7 +127,7 @@ export function getEncryptionMaterials<S extends SupportedAlgorithmSuites>({
*/
const testEntry = {
response: material,
now: Date.now(),
now: clockFor(this._cache)(),
messagesEncrypted: 1,
bytesEncrypted: plaintextLength,
}
Expand Down Expand Up @@ -193,7 +194,7 @@ export function cacheEntryHasExceededLimits<
this: CachingMaterialsManager<S>,
{ now, messagesEncrypted, bytesEncrypted }: Entry<S>
): boolean {
const age = Date.now() - now
const age = clockFor(this._cache)() - now
return (
age > this._maxAge ||
messagesEncrypted > this._maxMessagesEncrypted ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,184 +1,19 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import LRU from 'lru-cache'
import {
EncryptionMaterial,
DecryptionMaterial,
SupportedAlgorithmSuites,
needs,
isEncryptionMaterial,
isDecryptionMaterial,
BranchKeyMaterial,
isBranchKeyMaterial,
} from '@aws-crypto/material-management'

import {
CryptographicMaterialsCache,
Entry,
EncryptionMaterialEntry,
DecryptionMaterialEntry,
BranchKeyMaterialEntry,
} from './cryptographic_materials_cache'

// define a broader type for local CMC entries that encompass BranchKeyMaterial
// entries as well
type LocalCmcEntry<S extends SupportedAlgorithmSuites> =
| BranchKeyMaterialEntry
| Entry<S>
import { SupportedAlgorithmSuites } from '@aws-crypto/material-management'
import { CryptographicMaterialsCache } from './cryptographic_materials_cache'
import { localCryptographicMaterialsCache } from './local_cryptographic_materials_cache'

export function getLocalCryptographicMaterialsCache<
S extends SupportedAlgorithmSuites
>(
capacity: number,
proactiveFrequency: number = 1000 * 60
): CryptographicMaterialsCache<S> {
const cache = new LRU<string, LocalCmcEntry<S>>({
max: capacity,
dispose(_key, value) {
/* Zero out the unencrypted dataKey, when the material is removed from the cache. */
value.response.zeroUnencryptedDataKey()
},
})

/* It is not a guarantee that the last item in the LRU will be the Oldest Item.
* But such degenerative cases are not my concern.
* The LRU will not return things that are too old,
* so all this is just to try and proactively dispose material.
*
* To be clear, as an example say I add 9 items at T=0.
* If the MaxAge is 60 minutes, and at T=59 I add a 10th item.
* Then get each of the other 9 items.
* Now, at T=60, `mayEvictTail` will check the age of the tail
* and not evict it because the item has not aged out.
* If there is no get activity,
* it will take until T=120 before I again begin evicting items.
*/
;(function proactivelyTryAndEvictTail() {
const timeout = setTimeout(() => {
mayEvictTail()
proactivelyTryAndEvictTail()
}, proactiveFrequency)
/* In Node.js the event loop will _only_ exit if there are no outstanding events.
* This means that if I did nothing the event loop would *always* be blocked.
* This is unfortunate and very bad for things like Lambda.
* So, I tell Node.js to not wait for this timer.
* See: https://nodejs.org/api/timers.html#timers_timeout_unref
*/
// @ts-ignore
timeout.unref && timeout.unref()
})()

return {
putEncryptionMaterial(
key: string,
material: EncryptionMaterial<S>,
plaintextLength: number,
maxAge?: number
) {
/* Precondition: putEncryptionMaterial plaintextLength can not be negative. */
needs(plaintextLength >= 0, 'Malformed plaintextLength')
/* Precondition: Only cache EncryptionMaterial. */
needs(isEncryptionMaterial(material), 'Malformed response.')
/* Precondition: Only cache EncryptionMaterial that is cacheSafe. */
needs(material.suite.cacheSafe, 'Can not cache non-cache safe material')
const entry = Object.seal({
response: material,
bytesEncrypted: plaintextLength,
messagesEncrypted: 1,
now: Date.now(),
})

cache.set(key, entry, maxAge)
},

putDecryptionMaterial(
key: string,
material: DecryptionMaterial<S>,
maxAge?: number
) {
/* Precondition: Only cache DecryptionMaterial. */
needs(isDecryptionMaterial(material), 'Malformed response.')
/* Precondition: Only cache DecryptionMaterial that is cacheSafe. */
needs(material.suite.cacheSafe, 'Can not cache non-cache safe material')
const entry = Object.seal({
response: material,
bytesEncrypted: 0,
messagesEncrypted: 0,
now: Date.now(),
})

cache.set(key, entry, maxAge)
},

putBranchKeyMaterial(
key: string,
material: BranchKeyMaterial,
maxAge?: number
): void {
/* Precondition: Only cache BranchKeyMaterial */
needs(isBranchKeyMaterial(material), 'Malformed response.')

const entry = Object.seal({
response: material,
now: Date.now(),
})

cache.set(key, entry, maxAge)
},

getEncryptionMaterial(key: string, plaintextLength: number) {
/* Precondition: plaintextLength can not be negative. */
needs(plaintextLength >= 0, 'Malformed plaintextLength')
const entry = cache.get(key)
/* Check for early return (Postcondition): If this key does not have an EncryptionMaterial, return false. */
if (!entry) return false
/* Postcondition: Only return EncryptionMaterial. */
needs(isEncryptionMaterial(entry.response), 'Malformed response.')

const encryptionMaterialEntry = entry as EncryptionMaterialEntry<S>
encryptionMaterialEntry.bytesEncrypted += plaintextLength
encryptionMaterialEntry.messagesEncrypted += 1

return entry as EncryptionMaterialEntry<S>
},

getDecryptionMaterial(key: string) {
const entry = cache.get(key)
/* Check for early return (Postcondition): If this key does not have a DecryptionMaterial, return false. */
if (!entry) return false
/* Postcondition: Only return DecryptionMaterial. */
needs(isDecryptionMaterial(entry.response), 'Malformed response.')

return entry as DecryptionMaterialEntry<S>
},

getBranchKeyMaterial(key: string): BranchKeyMaterialEntry | false {
const entry = cache.get(key)

/* Postcondition: If this key does not have a BranchKeyMaterial, return false */
if (!entry) return false

/* Postcondition: Only return BranchKeyMaterial */
needs(isBranchKeyMaterial(entry.response), 'Malformed response.')
return entry as BranchKeyMaterialEntry
},

del(key: string) {
cache.del(key)
},
}

function mayEvictTail() {
// @ts-ignore
const { tail } = cache.dumpLru()
/* Check for early return (Postcondition) UNTESTED: If there is no tail, then the cache is empty. */
if (!tail) return
/* The underlying Yallist tail Node has a `value`.
* This value is a lru-cache Entry and has a `key`.
*/
const { key } = tail.value
// Peek will evict, but not update the "recently used"-ness of the key.
cache.peek(key)
}
return localCryptographicMaterialsCache(
capacity,
proactiveFrequency,
Date.now
)
}
Loading
Loading