@@ -86,15 +86,11 @@ public String protect(String value, String policyName) {
8686 * If untagged, the policyName must be provided.
8787 */
8888 public String access (String protectedValue ) {
89- // Try to find a tag match
89+ // Tag is just the first N chars of the string
9090 for (Map .Entry <String , Policy > e : tagIndex .entrySet ()) {
9191 String tag = e .getKey ();
92- Policy policy = e .getValue ();
93-
94- // Extract first N encryptable chars and check if they match the tag
95- String extracted = extractTag (protectedValue , policy .alphabet (), tag .length ());
96- if (tag .equals (extracted )) {
97- return accessWithPolicy (protectedValue , policy , true );
92+ if (protectedValue .length () > tag .length () && protectedValue .startsWith (tag )) {
93+ return accessWithPolicy (protectedValue , e .getValue (), true );
9894 }
9995 }
10096 throw new IllegalArgumentException ("No matching tag found. Use access(value, policyName) for untagged values." );
@@ -109,78 +105,55 @@ public String access(String protectedValue, String policyName) {
109105 return accessWithPolicy (protectedValue , policy , policy .tagEnabled ());
110106 }
111107
112- // -- Internal: FF1 protect --
113-
114- private String protectFf1 (String value , Policy policy ) {
115- try {
116- byte [] key = keyProvider .resolve (policy .keyRef ());
117- String alphabet = policy .alphabet ();
118-
119- // Separate encryptable chars from passthrough chars
120- StringBuilder encryptable = new StringBuilder ();
121- boolean [] isPassthrough = new boolean [value .length ()];
122- char [] passthroughChars = new char [value .length ()];
123-
124- for (int i = 0 ; i < value .length (); i ++) {
125- char c = value .charAt (i );
126- if (alphabet .indexOf (c ) >= 0 ) {
127- encryptable .append (c );
128- isPassthrough [i ] = false ;
129- } else {
130- isPassthrough [i ] = true ;
131- passthroughChars [i ] = c ;
132- }
133- }
134-
135- FF1 ff1 = new FF1 (key , new byte [0 ], alphabet );
136- String encrypted = ff1 .encrypt (encryptable .toString ());
137-
138- // Prepend tag to encrypted chars
139- String taggedCipher = policy .tagEnabled () ? policy .tag () + encrypted : encrypted ;
140-
141- // Reinsert passthrough chars at original positions
142- return reinsertPassthrough (taggedCipher , isPassthrough , passthroughChars , value .length (),
143- policy .tagEnabled () ? policy .tagLength () : 0 );
144- } catch (IllegalArgumentException e ) {
145- throw e ;
146- } catch (Exception e ) {
147- throw new RuntimeException ("FF1 encryption failed: " + e .getMessage (), e );
148- }
149- }
108+ // -- Internal: FPE protect (FF1 / FF3) --
150109
151- // -- Internal: FF3 protect --
110+ private String protectFf1 (String value , Policy policy ) { return protectFpe (value , policy , false ); }
111+ private String protectFf3 (String value , Policy policy ) { return protectFpe (value , policy , true ); }
152112
153- private String protectFf3 (String value , Policy policy ) {
113+ private String protectFpe (String value , Policy policy , boolean ff3 ) {
154114 try {
155115 byte [] key = keyProvider .resolve (policy .keyRef ());
156116 String alphabet = policy .alphabet ();
157117
118+ // 1. Strip passthroughs, record positions
119+ int [] ptPositions = new int [value .length ()];
120+ char [] ptChars = new char [value .length ()];
121+ int ptCount = 0 ;
158122 StringBuilder encryptable = new StringBuilder ();
159- boolean [] isPassthrough = new boolean [value .length ()];
160- char [] passthroughChars = new char [value .length ()];
161-
162123 for (int i = 0 ; i < value .length (); i ++) {
163124 char c = value .charAt (i );
164125 if (alphabet .indexOf (c ) >= 0 ) {
165126 encryptable .append (c );
166127 } else {
167- isPassthrough [i ] = true ;
168- passthroughChars [i ] = c ;
128+ ptPositions [ptCount ] = i ;
129+ ptChars [ptCount ] = c ;
130+ ptCount ++;
169131 }
170132 }
171133
172- // FF3 requires 8-byte tweak -- use zeroes as default
173- FF3 ff3 = new FF3 (key , new byte [8 ], alphabet );
174- String encrypted = ff3 .encrypt (encryptable .toString ());
134+ // 2. Encrypt
135+ String encrypted ;
136+ if (ff3 ) {
137+ encrypted = new FF3 (key , new byte [8 ], alphabet ).encrypt (encryptable .toString ());
138+ } else {
139+ encrypted = new FF1 (key , new byte [0 ], alphabet ).encrypt (encryptable .toString ());
140+ }
175141
176- String taggedCipher = policy .tagEnabled () ? policy .tag () + encrypted : encrypted ;
142+ // 3. Reinsert passthroughs at original positions
143+ StringBuilder withPt = new StringBuilder (encrypted );
144+ for (int i = 0 ; i < ptCount ; i ++) {
145+ withPt .insert (ptPositions [i ], ptChars [i ]);
146+ }
177147
178- return reinsertPassthrough (taggedCipher , isPassthrough , passthroughChars , value .length (),
179- policy .tagEnabled () ? policy .tagLength () : 0 );
148+ // 4. Prepend tag
149+ if (policy .tagEnabled ()) {
150+ return policy .tag () + withPt .toString ();
151+ }
152+ return withPt .toString ();
180153 } catch (IllegalArgumentException e ) {
181154 throw e ;
182155 } catch (Exception e ) {
183- throw new RuntimeException ("FF3 encryption failed: " + e .getMessage (), e );
156+ throw new RuntimeException ("FPE encryption failed: " + e .getMessage (), e );
184157 }
185158 }
186159
@@ -259,142 +232,57 @@ private String accessWithPolicy(String protectedValue, Policy policy, boolean ha
259232 }
260233
261234 private String accessFf1 (String protectedValue , Policy policy , boolean hasTag ) {
262- try {
263- byte [] key = keyProvider .resolve (policy .keyRef ());
264- String alphabet = policy .alphabet ();
265-
266- // Extract encryptable chars (skip passthrough)
267- StringBuilder encryptable = new StringBuilder ();
268- for (int i = 0 ; i < protectedValue .length (); i ++) {
269- char c = protectedValue .charAt (i );
270- if (alphabet .indexOf (c ) >= 0 ) {
271- encryptable .append (c );
272- }
273- }
274-
275- String cipherChars = encryptable .toString ();
276-
277- // Strip tag if present
278- if (hasTag && policy .tag () != null ) {
279- cipherChars = cipherChars .substring (policy .tagLength ());
280- }
281-
282- FF1 ff1 = new FF1 (key , new byte [0 ], alphabet );
283- String decrypted = ff1 .decrypt (cipherChars );
284-
285- // Reinsert passthrough chars at their original positions (minus tag offset)
286- return reinsertPassthroughForAccess (decrypted , protectedValue , alphabet ,
287- hasTag ? policy .tagLength () : 0 );
288- } catch (IllegalArgumentException e ) {
289- throw e ;
290- } catch (Exception e ) {
291- throw new RuntimeException ("FF1 decryption failed: " + e .getMessage (), e );
292- }
235+ return accessFpe (protectedValue , policy , hasTag , false );
293236 }
294237
295238 private String accessFf3 (String protectedValue , Policy policy , boolean hasTag ) {
239+ return accessFpe (protectedValue , policy , hasTag , true );
240+ }
241+
242+ private String accessFpe (String protectedValue , Policy policy , boolean hasTag , boolean ff3 ) {
296243 try {
297244 byte [] key = keyProvider .resolve (policy .keyRef ());
298245 String alphabet = policy .alphabet ();
299246
247+ // 1. Strip tag (first N chars of the full string)
248+ String withoutTag = hasTag ? protectedValue .substring (policy .tagLength ()) : protectedValue ;
249+
250+ // 2. Strip passthroughs, record positions
251+ int [] ptPositions = new int [withoutTag .length ()];
252+ char [] ptChars = new char [withoutTag .length ()];
253+ int ptCount = 0 ;
300254 StringBuilder encryptable = new StringBuilder ();
301- for (int i = 0 ; i < protectedValue .length (); i ++) {
302- char c = protectedValue .charAt (i );
255+ for (int i = 0 ; i < withoutTag .length (); i ++) {
256+ char c = withoutTag .charAt (i );
303257 if (alphabet .indexOf (c ) >= 0 ) {
304258 encryptable .append (c );
259+ } else {
260+ ptPositions [ptCount ] = i ;
261+ ptChars [ptCount ] = c ;
262+ ptCount ++;
305263 }
306264 }
307265
308- String cipherChars = encryptable .toString ();
309- if (hasTag && policy .tag () != null ) {
310- cipherChars = cipherChars .substring (policy .tagLength ());
266+ // 3. Decrypt
267+ String decrypted ;
268+ if (ff3 ) {
269+ decrypted = new FF3 (key , new byte [8 ], alphabet ).decrypt (encryptable .toString ());
270+ } else {
271+ decrypted = new FF1 (key , new byte [0 ], alphabet ).decrypt (encryptable .toString ());
311272 }
312273
313- FF3 ff3 = new FF3 (key , new byte [8 ], alphabet );
314- String decrypted = ff3 .decrypt (cipherChars );
274+ // 4. Reinsert passthroughs at original positions
275+ StringBuilder result = new StringBuilder (decrypted );
276+ for (int i = 0 ; i < ptCount ; i ++) {
277+ result .insert (ptPositions [i ], ptChars [i ]);
278+ }
315279
316- return reinsertPassthroughForAccess (decrypted , protectedValue , alphabet ,
317- hasTag ? policy .tagLength () : 0 );
280+ return result .toString ();
318281 } catch (IllegalArgumentException e ) {
319282 throw e ;
320283 } catch (Exception e ) {
321- throw new RuntimeException ("FF3 decryption failed: " + e .getMessage (), e );
322- }
323- }
324-
325- // -- Passthrough helpers --
326-
327- /**
328- * Extract the first N encryptable characters from a value (skipping passthrough chars).
329- */
330- private String extractTag (String value , String alphabet , int tagLength ) {
331- StringBuilder sb = new StringBuilder (tagLength );
332- for (int i = 0 ; i < value .length () && sb .length () < tagLength ; i ++) {
333- if (alphabet .indexOf (value .charAt (i )) >= 0 ) {
334- sb .append (value .charAt (i ));
335- }
336- }
337- return sb .toString ();
338- }
339-
340- /**
341- * Reinsert passthrough characters from the original value into the cipher output.
342- * The output is longer than the input by tagLength chars (the tag adds characters).
343- */
344- private String reinsertPassthrough (String cipherChars , boolean [] isPassthrough ,
345- char [] passthroughChars , int origLen , int tagExtra ) {
346- int totalLen = origLen + tagExtra ;
347- StringBuilder result = new StringBuilder (totalLen );
348- int cipherIdx = 0 ;
349-
350- // Build the output: passthrough chars stay in their original positions,
351- // cipher chars fill the rest, extra tag chars extend the end
352- for (int i = 0 ; i < origLen ; i ++) {
353- if (isPassthrough [i ]) {
354- result .append (passthroughChars [i ]);
355- } else {
356- if (cipherIdx < cipherChars .length ()) {
357- result .append (cipherChars .charAt (cipherIdx ++));
358- }
359- }
360- }
361- // Append remaining cipher chars (from the tag extension)
362- while (cipherIdx < cipherChars .length ()) {
363- result .append (cipherChars .charAt (cipherIdx ++));
364- }
365-
366- return result .toString ();
367- }
368-
369- /**
370- * For access: reconstruct plaintext with passthrough chars from the protected value.
371- * The protected value is longer than the original by tagLength.
372- */
373- private String reinsertPassthroughForAccess (String decrypted , String protectedValue ,
374- String alphabet , int tagExtra ) {
375- // The original had (protectedValue.length - tagExtra) total chars
376- int origLen = protectedValue .length () - tagExtra ;
377- StringBuilder result = new StringBuilder (origLen );
378- int decIdx = 0 ;
379- int encCount = 0 ; // count of encryptable chars seen
380-
381- // Walk the protected value, skip tag chars, preserve passthroughs
382- for (int i = 0 ; i < protectedValue .length () && result .length () < origLen ; i ++) {
383- char c = protectedValue .charAt (i );
384- if (alphabet .indexOf (c ) >= 0 ) {
385- encCount ++;
386- if (encCount <= tagExtra ) {
387- continue ; // skip tag chars
388- }
389- if (decIdx < decrypted .length ()) {
390- result .append (decrypted .charAt (decIdx ++));
391- }
392- } else {
393- result .append (c ); // passthrough
394- }
284+ throw new RuntimeException ("FPE decryption failed: " + e .getMessage (), e );
395285 }
396-
397- return result .toString ();
398286 }
399287
400288 private static String bytesToHex (byte [] bytes ) {
0 commit comments