Skip to content

Conversation

@sophia-chen-ttd
Copy link
Contributor

@sophia-chen-ttd sophia-chen-ttd commented Aug 22, 2025

Implemented key rotation set behind a feature flag.
When flag is enabled:

  • CurrentSalt is null
  • CurrentKey is populated. KeyId is incremented from 0 until it reaches 16777215 where it wraps back to 0

When flag is disabled:

  • CurrentSalt is populated
  • CurrentKey is null

--

  • PreviousSalt is populated if CurrentSalt existed before rotation, removed after 90 days
  • PreviousKey is populated if CurrentKey existed before rotation, removed after 90 days

TESTS:

  • testRotateFromSaltToKey: Tests that currentKey and previousSalt are populated appropriately, and currentSalt is null.
  • testKeyRotationInitialKeyIdPopulation: Tests that initial keyIds are set starting from 0.
  • testKeyRotationKeyIdIncrementation: Tests that keyIds are incremented from last used keyId.
  • testKeyRotationKeyIdWrap: Tests that keyIds are wrapped back to 0 after reaching the max key id.
  • testKeyRotationKeyIdWrapManyBuckets: Tests that key ids are appropriately incremented given a salt file with wrapped but non-consecutive key ids.
  • testKeyRotationPopulatePreviousKey: Tests previousKey is populated for a buckets that contain currentKey
  • testKeyRotationPreviousKeyVisibleFor90Days: Tests previousKey is only available for 90 days after key rotation
  • testKeyRotationSaltToKeyRotation: Tests that with feature flag disabled, a rotating bucket with keys will populate currentSalt and previousKey, and currentKey is null.

Other:

  • Added new salts file in localstack with 1001 salts based on prod salt distribution for more accurate rotation simulations

@sophia-chen-ttd sophia-chen-ttd changed the title sch-UID2-5851 migration to key rotation sch-UID2-5851 migration from salt to key rotation Aug 22, 2025
this.lastActiveKeyId = new AtomicInteger(getLastActiveKeyId(salts));
}

private int getLastActiveKeyId(SaltEntry[] salts) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be static

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

public class KeyIdGenerator {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worthwhile to add a comment somewhere in this class about the assumptions of key IDs in the latest rotated buckets and the intended outcomes of this logic:

  • Key IDs from the latest buckets were the last allocated key IDs
  • These key IDs should always be consecutive
  • If the last allocated key ID contains 16777215, we should wrap around
  • Continuing to increment from the last "highest" key ID will result in monotonic incrementation of key IDs for all newly rotated buckets

Hopefully my understanding is correct, it took me a while to grasp all of these assumptions and outcomes so a comment would help future readers of this code

var refreshFrom = calculateRefreshFrom(oldSalt, targetDate);
var currentSalt = calculateCurrentSalt(oldSalt, shouldRotate);
var previousSalt = calculatePreviousSalt(oldSalt, shouldRotate, targetDate);
var currentKey = calculateCurrentKey(oldSalt, shouldRotate, keyIdGenerator);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename these and other related vars to something like "keySalt"? I keep forgetting this includes both the encryption key and encryption salt

}

private String calculateCurrentSalt(SaltEntry salt, boolean shouldRotate) throws Exception {
return shouldRotate ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: nested ternaries are hard to read, just use if statements

}

@Test
void testKeyRotationKeyIdIncrementation() throws Exception {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consolidate testKeyRotationKeyIdIncrementation, testKeyRotationKeyIdWrap, testKeyRotationKeyIdWrapManyBuckets?

}

@Test
void testRotateFromSaltToKey() throws Exception {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consolidate testRotateFromSaltToKey and testKeyRotationInitialKeyIdPopulation?

private final AtomicInteger lastActiveKeyId;

public KeyIdGenerator(SaltEntry[] buckets) {
this.lastActiveKeyId = new AtomicInteger(getLastActiveKeyId(buckets));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe store next active key ID


/**
* Intended outcomes of KeyIdGenerator:
* - Key ids are always consecutive, starting from 0
Copy link

@vishalegbert-ttd vishalegbert-ttd Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key IDs are always monotonically increasing

* - When the last allocated key id reaches 16777215, the next key id will wrap around to 0
* - Continuing to increment from the highest key id will result in monotonic incrementation of key ids for all newly rotated buckets
*
* Assumptions:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumptions go first

private static int getLastActiveKeyId(SaltEntry[] buckets) {
long maxLastUpdated = Arrays.stream(buckets).mapToLong(SaltEntry::lastUpdated).max().orElse(0);
int[] lastActiveKeyIdsSorted = Arrays.stream(buckets)
.filter(s -> s.lastUpdated() == maxLastUpdated && s.currentKey() != null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this more robust to turning off the feature switch

@sophia-chen-ttd sophia-chen-ttd merged commit 33673fd into main Aug 29, 2025
4 checks passed
@sophia-chen-ttd sophia-chen-ttd deleted the sch-UID2-5851-migration-to-key-rotation branch August 29, 2025 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants