How to calculate the maximum length of the output of the Laravel encryption method?
Looking through the source code for Crypt::encryptString
, we can see that the final result will be a base64 encoded JSON object that has the following structure:
{ "iv": "<128 bits in base64>", "value": "<x bits in base64>", "mac": "<256 bits in hex>" }
Where the value of x
is ceil(n / 128) * 128
where n
is the number of bits in the original plaintext.
This means that, for an input plaintext of length 1, the size of the output should be:
- 24 characters for the IV (base64).
- 24 characters for the ciphertext (base64).
- 64 characters for the SHA256 mac (hex).
- 10 characters for the names of the JSON fields.
- 19 characters of extra JSON characters e.g.
{
,"
,:
. - A final round of base64 encoding of the whole thing... (
ceil(141 / 3) * 4
)
Gives a total of 188. The fluctuations up to 192 are odd - your inputs are not changing in size at all (since the plaintext should always be 16 bytes between 0 - 15 length).
The real problem - can $iv and $value contain more than a single '/'?
Sure. Your worst case for the IV is the IV FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
(hex), which has a Base64 value of /////////////////////w==
.
21 forward slashes * extra 3 bytes each = 63 extra bytes.
For the HMAC-SHA-2-256, you could get 32 bytes of 0xFF (worst case), which is //////////////////////////////////////////8=
in base64.
42 forward slashes => 126 extra bytes.
For the ciphertext, again, the entire output could be (but likely isn't) FF FF ... FF
. All one letter inputs (no matter what encoding) are a single block of ciphertext, making the output be /////////////////////w==
again (+63).
The generalized formula for the maximum seems to be
- IV: 24 + 63 = 87
- HMAC: 24 + 63 = 87
- JSON Property Names: 10
- JSON Structure: 19
- Ciphertext:
ceil(ceil((n+1) / 16) * 16 / 3) * 4 * 4
(I usedn
as bytes. padded ciphertext is ceil((n+1) / blocksize) * blocksize, base64 is 4 * ceil(data / 3), extra *4 is "everything is slashes") - Base64 it all again: 4 * ceil(sum / 3)
= 4 * ceil((4 * 4 * ceil(16 * ceil((n + 1) / 16) / 3) + 203) / 3)
For n=1
that produces 400 bytes. The actual maximum is (I think) 388, because the ciphertext formula is counting 24 slashes as the worst case when 21 is the worst case. So the true supremum needs to call the ciphertext something more complicated involving floor, ceiling, and subtraction.
Note I'm going to award the bounty to @Luke Joshua Park as he got me closest to what ended up being the (closest thing to a) solution, which is to follow.
(Not a) solution
The answer is, there is no concrete answer, not without unknowns and variance. Across the three people looking at this at the time of writing (myself, Luke, and bartonjs) there was still some doubt to a 100% accurate solution.
The question was posed to figure out a reliable type and size to store encrypted data, ideally in a database independent fashion (I didn't want to specify a particular database, as I wanted to know and understand how to calculate a length regardless of the way it was persisted).
However, even strings of the smallest lengths turned out to be quite long in the worst case scenario (where a random $iv was created containing many slashes - unlikely or not, it was possible). Possible encrypted strings of n=1
possibly being 400 bytes long mean that a varchar
will never be the right answer.
So... what should be done?
So, instead, it seems best, most consistent and most reliable to store encrypted data as a text field and not a varchar (in mysql land), regardless of the length of the original string. This is a disappointingly boring answer with no fancy maths involved. It's not the answer I would like to accept, but makes the most sense.
But, what about passwords?
In a brief moment of stupidity, I thought, but what about the password field? That is a varchar
. But of course that is a hashed value, not an encrypted value (I hadn't had enough coffee when that thought popped into my head, ok?)