@@ -55,13 +55,21 @@ private function __construct(array $config)
5555 $ this ->headerIndex [$ header ] = $ name ;
5656 }
5757
58+ $ tweakHex = $ cfg ['tweak ' ] ?? null ;
59+ $ tweak = (is_string ($ tweakHex ) && $ tweakHex !== '' ) ? hex2bin ($ tweakHex ) : null ;
60+ if ($ tweakHex !== null && $ tweak === false ) {
61+ throw new \InvalidArgumentException ("configuration error: invalid hex tweak in ' {$ name }' " );
62+ }
63+
5864 $ this ->configurations [$ name ] = [
65+ 'name ' => $ name ,
5966 'engine ' => $ cfg ['engine ' ] ?? 'ff1 ' ,
6067 'alphabet ' => self ::resolveAlphabet ($ cfg ['alphabet ' ] ?? null ),
6168 'key_ref ' => $ cfg ['key_ref ' ] ?? null ,
6269 'header ' => $ header ,
6370 'header_enabled ' => $ headerEnabled ,
6471 'header_length ' => (int )($ cfg ['header_length ' ] ?? 3 ),
72+ 'tweak ' => $ tweak ,
6573 'pattern ' => $ cfg ['pattern ' ] ?? null ,
6674 'algorithm ' => $ cfg ['algorithm ' ] ?? 'sha256 ' ,
6775 ];
@@ -163,6 +171,22 @@ private function accessWithConfiguration(string $protectedValue, array $configur
163171
164172 // ── FPE ──
165173
174+ /**
175+ * Require an exact-length tweak for FF3 / FF3-1. Missing or wrong-length
176+ * tweaks are a hard error — no silent zero-fill. FF1 tweak is optional
177+ * per NIST SP 800-38G and is handled separately.
178+ */
179+ private static function requireTweak (array $ configuration , int $ expectedLen , string $ label ): string
180+ {
181+ $ tweak = $ configuration ['tweak ' ] ?? null ;
182+ if (!is_string ($ tweak ) || strlen ($ tweak ) !== $ expectedLen ) {
183+ throw new \InvalidArgumentException (
184+ "configuration ' {$ configuration ['name ' ]}' is missing required 'tweak' ( {$ label } needs {$ expectedLen } bytes) "
185+ );
186+ }
187+ return $ tweak ;
188+ }
189+
166190 private static bool $ ff3Warned = false ;
167191
168192 /** Emit the FF3 deprecation warning to stderr, once per process. */
@@ -187,11 +211,11 @@ private function protectFpe(string $value, array $configuration): string
187211
188212 if ($ configuration ['engine ' ] === 'ff3 ' ) {
189213 $ this ->warnFf3Deprecated ();
190- $ cipher = new FF3 ($ key , str_repeat ( "\x00" , 8 ), $ alphabet );
214+ $ cipher = new FF3 ($ key , self :: requireTweak ( $ configuration , 8 , ' FF3 ' ), $ alphabet );
191215 } elseif ($ configuration ['engine ' ] === 'ff31 ' ) {
192- $ cipher = new FF31 ($ key , str_repeat ( "\x00" , 7 ), $ alphabet );
216+ $ cipher = new FF31 ($ key , self :: requireTweak ( $ configuration , 7 , ' FF3-1 ' ), $ alphabet );
193217 } else {
194- $ cipher = new FF1 ($ key , '' , $ alphabet );
218+ $ cipher = new FF1 ($ key , $ configuration [ ' tweak ' ] ?? '' , $ alphabet );
195219 }
196220 $ encrypted = $ cipher ->encrypt ($ encryptable );
197221
@@ -224,11 +248,11 @@ private function accessFpe(string $rawCiphertext, array $configuration): string
224248
225249 if ($ configuration ['engine ' ] === 'ff3 ' ) {
226250 $ this ->warnFf3Deprecated ();
227- $ cipher = new FF3 ($ key , str_repeat ( "\x00" , 8 ), $ alphabet );
251+ $ cipher = new FF3 ($ key , self :: requireTweak ( $ configuration , 8 , ' FF3 ' ), $ alphabet );
228252 } elseif ($ configuration ['engine ' ] === 'ff31 ' ) {
229- $ cipher = new FF31 ($ key , str_repeat ( "\x00" , 7 ), $ alphabet );
253+ $ cipher = new FF31 ($ key , self :: requireTweak ( $ configuration , 7 , ' FF3-1 ' ), $ alphabet );
230254 } else {
231- $ cipher = new FF1 ($ key , '' , $ alphabet );
255+ $ cipher = new FF1 ($ key , $ configuration [ ' tweak ' ] ?? '' , $ alphabet );
232256 }
233257 $ decrypted = $ cipher ->decrypt ($ encryptable );
234258
0 commit comments