|
| 1 | +import { createHash } from 'crypto'; |
| 2 | + |
| 3 | +import { |
| 4 | + AllocationDto, |
| 5 | + BanditFlagVariationDto, |
| 6 | + BanditReferenceDto, |
| 7 | + FlagDto, |
| 8 | + RuleDto, |
| 9 | + ShardDto, |
| 10 | + SplitDto, |
| 11 | + UFCFormatEnum, |
| 12 | + UniversalFlagConfig, |
| 13 | + VariationDto, |
| 14 | + ITargetingRuleCondition |
| 15 | +} from './ufc.dto'; |
| 16 | + |
| 17 | +export function md5Hash(input: string): string { |
| 18 | + return createHash('md5').update(input).digest('hex'); |
| 19 | +} |
| 20 | + |
| 21 | +export function base64Encode(input: string): string { |
| 22 | + return Buffer.from(input).toString('base64'); |
| 23 | +} |
| 24 | + |
| 25 | +export function obfuscateUniversalFlagConfig(ufc: UniversalFlagConfig): UniversalFlagConfig { |
| 26 | + // Start building a new, obfuscated, UniversalFlagConfig |
| 27 | + // We opted to do this instead of starting with a copy and mutating in-place for two reasons: |
| 28 | + // 1. The code is easier to understand as it doesn't mutate in-place |
| 29 | + // 2. If anything sensitive is added to the UFC but forgotten to be explicitly obfuscated, it could slip through |
| 30 | + const obfuscatedUfc = new UniversalFlagConfig(); |
| 31 | + |
| 32 | + // update format |
| 33 | + obfuscatedUfc.format = UFCFormatEnum.CLIENT; |
| 34 | + |
| 35 | + // copy over fields that are not obfuscated |
| 36 | + obfuscatedUfc.environment = ufc.environment; |
| 37 | + if (ufc.createdAt) { |
| 38 | + obfuscatedUfc.createdAt = ufc.createdAt; |
| 39 | + } |
| 40 | + |
| 41 | + // Obfuscate flags |
| 42 | + obfuscatedUfc.flags = {}; |
| 43 | + Object.values(ufc.flags).forEach((flagDto) => { |
| 44 | + const obfuscatedFlagDto = obfuscateFlag(flagDto); |
| 45 | + obfuscatedUfc.flags[obfuscatedFlagDto.key] = obfuscatedFlagDto; |
| 46 | + }); |
| 47 | + |
| 48 | + // Obfuscate bandits |
| 49 | + if (ufc.bandits) { |
| 50 | + const obfuscatedBandits: Record<string, BanditFlagVariationDto[]> = {}; |
| 51 | + Object.entries(ufc.bandits).forEach(([banditKey, banditFlagVariations]) => { |
| 52 | + obfuscatedBandits[md5Hash(banditKey)] = banditFlagVariations.map( |
| 53 | + obfuscateBanditFlagVariation, |
| 54 | + ); |
| 55 | + }); |
| 56 | + obfuscatedUfc.bandits = obfuscatedBandits; |
| 57 | + } |
| 58 | + if (ufc.banditReferences) { |
| 59 | + const obfuscatedBanditReferences: Record<string, BanditReferenceDto> = {}; |
| 60 | + Object.entries(ufc.banditReferences).forEach(([banditKey, banditReference]) => { |
| 61 | + obfuscatedBanditReferences[md5Hash(banditKey)] = obfuscateBanditReference(banditReference); |
| 62 | + }); |
| 63 | + obfuscatedUfc.banditReferences = obfuscatedBanditReferences; |
| 64 | + } |
| 65 | + return obfuscatedUfc; |
| 66 | +} |
| 67 | + |
| 68 | +function obfuscateFlag(flagDto: FlagDto): FlagDto { |
| 69 | + const obfuscatedFlagDto = new FlagDto(); |
| 70 | + |
| 71 | + // We hash the flag key as the SDK will have an exact value to hash and compare |
| 72 | + obfuscatedFlagDto.key = md5Hash(flagDto.key); |
| 73 | + |
| 74 | + // copy over fields we are not obfuscating |
| 75 | + obfuscatedFlagDto.enabled = flagDto.enabled; |
| 76 | + obfuscatedFlagDto.variationType = flagDto.variationType; |
| 77 | + obfuscatedFlagDto.totalShards = flagDto.totalShards; |
| 78 | + |
| 79 | + // Obfuscate variations |
| 80 | + obfuscatedFlagDto.variations = {}; |
| 81 | + Object.values(flagDto.variations).forEach((variationDto) => { |
| 82 | + const obfuscatedVariationDto = obfuscateVariationDto(variationDto); |
| 83 | + obfuscatedFlagDto.variations[obfuscatedVariationDto.key] = obfuscatedVariationDto; |
| 84 | + }); |
| 85 | + |
| 86 | + // Obfuscate allocations (Note: we map as it's an array, and we want to preserve order) |
| 87 | + obfuscatedFlagDto.allocations = flagDto.allocations.map(obfuscateAllocation); |
| 88 | + |
| 89 | + return obfuscatedFlagDto; |
| 90 | +} |
| 91 | + |
| 92 | +function obfuscateVariationDto(variationDto: VariationDto): VariationDto { |
| 93 | + const obfuscatedVariationDto = new VariationDto(); |
| 94 | + obfuscatedVariationDto.key = base64Encode(variationDto.key); |
| 95 | + |
| 96 | + // Obfuscate any non-null/non-undefined (note loose comparison) values |
| 97 | + obfuscatedVariationDto.value = |
| 98 | + variationDto.value != null ? base64Encode(variationDto.value.toString()) : variationDto.value; |
| 99 | + |
| 100 | + // Copy over algorithmType, unobfuscated, if present |
| 101 | + if (variationDto.algorithmType) { |
| 102 | + obfuscatedVariationDto.algorithmType = variationDto.algorithmType; |
| 103 | + } |
| 104 | + |
| 105 | + return obfuscatedVariationDto; |
| 106 | +} |
| 107 | + |
| 108 | +function obfuscateAllocation(allocationDto: AllocationDto): AllocationDto { |
| 109 | + const obfuscatedAllocationDto = new AllocationDto(); |
| 110 | + obfuscatedAllocationDto.key = base64Encode(allocationDto.key); |
| 111 | + |
| 112 | + // Leave doLog unobfuscated |
| 113 | + obfuscatedAllocationDto.doLog = allocationDto.doLog; |
| 114 | + |
| 115 | + // Obfuscate startAt and endAt, if present |
| 116 | + if (allocationDto.startAt) { |
| 117 | + obfuscatedAllocationDto.startAt = base64Encode(allocationDto.startAt); |
| 118 | + } |
| 119 | + if (allocationDto.endAt) { |
| 120 | + obfuscatedAllocationDto.endAt = base64Encode(allocationDto.endAt); |
| 121 | + } |
| 122 | + |
| 123 | + // Obfuscate rules, if present, conserving order |
| 124 | + if (allocationDto.rules) { |
| 125 | + obfuscatedAllocationDto.rules = allocationDto.rules.map(obfuscateRule); |
| 126 | + } |
| 127 | + |
| 128 | + // Obfuscate splits, preserving order |
| 129 | + obfuscatedAllocationDto.splits = allocationDto.splits.map(obfuscateSplit); |
| 130 | + |
| 131 | + return obfuscatedAllocationDto; |
| 132 | +} |
| 133 | + |
| 134 | +function obfuscateRule(ruleDto: RuleDto): RuleDto { |
| 135 | + const obfuscatedRuleDto = new RuleDto(); |
| 136 | + |
| 137 | + // Obfuscate conditions, preserving order |
| 138 | + obfuscatedRuleDto.conditions = ruleDto.conditions.map(obfuscateConditionInPlace); |
| 139 | + return obfuscatedRuleDto; |
| 140 | +} |
| 141 | + |
| 142 | +function obfuscateConditionInPlace(condition: ITargetingRuleCondition): ITargetingRuleCondition { |
| 143 | + // Note: we return the object in one shot instead of building out the keys as it's an interface not a DTO object |
| 144 | + return { |
| 145 | + operator: md5Hash(condition.operator), // We can hash, as the SDK knows the set of possibilities |
| 146 | + attribute: md5Hash(condition.attribute), // We can hash, as the SDK has the exact attributes to check for exact matches |
| 147 | + value: obfuscateConditionValueForOperator(condition.value, condition.operator), // How we obfuscate depends on the operator |
| 148 | + }; |
| 149 | +} |
| 150 | + |
| 151 | +function obfuscateConditionValueForOperator<T extends ITargetingRuleCondition>( |
| 152 | + value: T['value'], |
| 153 | + operator: string, |
| 154 | +): string | string[] { |
| 155 | + if (['ONE_OF', 'NOT_ONE_OF', 'IS_NULL'].includes(operator)) { |
| 156 | + // We hash values when it's an exact comparison because we can compare hashes |
| 157 | + if (Array.isArray(value)) { |
| 158 | + return value.map(md5Hash); |
| 159 | + } else { |
| 160 | + return md5Hash(value.toString()); |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + // Everything else (e.g., inequalities, regular expressions) we need to just encode, so that we |
| 165 | + // can reverse for evaluation |
| 166 | + return base64Encode(value.toString()); |
| 167 | +} |
| 168 | + |
| 169 | +function obfuscateSplit(splitDto: SplitDto): SplitDto { |
| 170 | + const obfuscatedSplitDto = new SplitDto(); |
| 171 | + obfuscatedSplitDto.variationKey = base64Encode(splitDto.variationKey); |
| 172 | + |
| 173 | + // Obfuscate shards, if present, as their keys could contain sensitive information |
| 174 | + if (splitDto.shards) { |
| 175 | + obfuscatedSplitDto.shards = splitDto.shards.map(obfuscateShard); |
| 176 | + } |
| 177 | + |
| 178 | + // Obfuscate extra logging, if present |
| 179 | + if (splitDto.extraLogging) { |
| 180 | + obfuscatedSplitDto.extraLogging = {}; |
| 181 | + for (const [extraLoggingKey, extraLoggingValue] of Object.entries(splitDto.extraLogging)) { |
| 182 | + obfuscatedSplitDto.extraLogging[base64Encode(extraLoggingKey)] = base64Encode( |
| 183 | + extraLoggingValue, |
| 184 | + ); |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + return obfuscatedSplitDto; |
| 189 | +} |
| 190 | + |
| 191 | +function obfuscateShard(shardDto: ShardDto): ShardDto { |
| 192 | + const obfuscatedShardDto = new ShardDto(); |
| 193 | + |
| 194 | + // Copy over ranges unobfuscated |
| 195 | + obfuscatedShardDto.ranges = structuredClone(shardDto.ranges); |
| 196 | + |
| 197 | + // Obfuscate salt |
| 198 | + obfuscatedShardDto.salt = base64Encode(shardDto.salt); |
| 199 | + |
| 200 | + return obfuscatedShardDto; |
| 201 | +} |
| 202 | + |
| 203 | +function obfuscateBanditReference(properties: BanditReferenceDto) { |
| 204 | + const obfuscatedProperties = new BanditReferenceDto(); |
| 205 | + obfuscatedProperties.modelVersion = properties.modelVersion |
| 206 | + ? md5Hash(properties.modelVersion) |
| 207 | + : null; |
| 208 | + obfuscatedProperties.flagVariations = properties.flagVariations.map(obfuscateBanditFlagVariation); |
| 209 | + return obfuscatedProperties; |
| 210 | +} |
| 211 | + |
| 212 | +function obfuscateBanditFlagVariation(banditFlagVariation: BanditFlagVariationDto) { |
| 213 | + const obfuscatedBanditFlagVariation = new BanditFlagVariationDto(); |
| 214 | + // Everything can be hashed as we'll have the values and be doing direct lookups |
| 215 | + obfuscatedBanditFlagVariation.key = md5Hash(banditFlagVariation.key); |
| 216 | + obfuscatedBanditFlagVariation.flagKey = md5Hash(banditFlagVariation.flagKey); |
| 217 | + obfuscatedBanditFlagVariation.allocationKey = md5Hash(banditFlagVariation.allocationKey); |
| 218 | + obfuscatedBanditFlagVariation.variationKey = md5Hash(banditFlagVariation.variationKey); |
| 219 | + obfuscatedBanditFlagVariation.variationValue = md5Hash(banditFlagVariation.variationValue); |
| 220 | + |
| 221 | + return obfuscatedBanditFlagVariation; |
| 222 | +} |
0 commit comments