How many characters should a session key be for security? - security

I am generating a session key to be stored in a cookie using the following function:
function getRandomKey($length=32) {
$string = '';
$characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for ($i = 0; $i < $length; $i++) {
$string .= $characters[mt_rand(0, strlen($characters)-1)];
}
return $string;
}
If I were to generate a 1 digit key it would have:
26 lowercase + 26 uppercase + 0-10 = 62 options.
Therefore an 8 digit key would have 62^8 or 218,340,105,584,896 possible combinations.
1) Is there any rule of thumb on how many characters out I should go? The more the better, I know, but is 8 enough or should it be more like 32 characters, 64 etc.?
2) Are there any security concerns when using localStorage?
Thanks in advance!

These are two very different questions.
1) TL;DR: about 16 characters (case-sensitive) is ok for most purposes.
First, please if you can, avoid implementing session management. It is already done in many frameworks, including session id generation and more - use an existing, well-known implementation if you can, because it is not straightforward to get it right.
Now, it's all about entropy. You started out right by calculating the number of possible combinations. If you take log2 of that, you get how many bits of entropy that session id has. (Well, let's not go into entropy here...)
So one case-sensitive alphanumeric character ([a-zA-Z0-9]) has log2(62)=5.9542 bits of entropy, two characters two times more, and so on.
The time required for an attacker to guess a valid session id is:
(2^b + 1) / (2 * n * s)
Where 'b' is the available bits of entropy in the session id, 'n' is the number of guesses the attacker can make every second, and 's' is the number of valid session ids in the system.
In a large, distributed web application, potentially using a botnet, an attacker may be able to make n=100000 guesses a second, and there may be s=1 million valid session ids. You want the result to be several hundred years at the very least, say 300 (15768000000 seconds). (These are totally arbitrary values.)
This gives about b=70, so you need 70 bits of entropy. If each character has 5.9542 bits of entropy as discussed above, it gives about 12 for the required session id length, but you can just round it up to 16 to make sure. :)
As a rule of thumb, it is sometimes assumed that bits of entropy in a session id is half the length (in bits) of that session id. It is mostly a reasonable approximation without any calculation. :) Even more so, because sessuion ids are sometimes actual random numbers base64 or otherwise encoded. Different encodings usually give different results though.
Also make sure to use a cryptographic random number generator, otherwise entropy is much less. Note that mt_rand() is not cryptographically random, so the code in your question is vulnerable!
2) TL;DR Yes. (I suppose you mean using local storage for storing the session id.)
The best possible place to store a session id is a httpOnly, Secure cookie without an expiration (non-persistent), because Javascript cannot access it there (for example cross-site scripting doesn't affect a victim user's session id at least), and being non-persistent, it will be removed when the user closes the browser and will not be persisted to disk (well, mostly... but that's a long story).
If you use localStorage, any XSS will directly affect the session id, which is very valuable for an attacker. Also sessions will survive closing the browser, which is slightly unexpected - user sessuions might easily be hijacked on shared computers.
Note though that this depends on the use-case and the risk you want to take. While it would definitaly not be ok for a financial application where you can access and manage very sensitive data, it can be ok for less risky applications. You can also let the user decide ("remember me", in which case you put it into localStorage), but most users are not aware of the associated risk, so they can't make an informed decision.
Also note that sessionStorage is a little better, because the session id will be removed from the browser when it is closed, but it is still available to Javascript (XSS).

Related

How do I calculate TVL on-chain?

I am working on a anchor / solana program that provides liquidity to a number of pools, including saber.so and invariant.app. During the swap, I need to calculate the TVL, to provision a token at a fair exchange rate.
My question is: what is the best way to calculate TVL onchain?
The following are some approaches that I have in mind, each one with its shortcomings:
(1) Calculate off-chain, and provide this as an oracle:
We could calculate the TVL off-chain, and then provide this TVL as an oracle. The shortcomings are: chainlink (an oracle provider) on solana does not seem to support custom data-feeds, as is the case with ethereum.
Further more this solution increases the centralization of the app, it would be nice to have it on-chain. also there could be oracle-attacks which drain the reserves of the protocol.
(2) Have a giant list of liquidity-positions:
Another approach would be to keep track of all liquidity-positions that we as a protocol have provided liquidity in. Although this is possible, I believe that this would (very quickly), reach solana's account limit.
In this case, we would have a huge "state-"account, which tracks the following variables per pool:
token1_mint: Pubkey
token2_mint: Pubkey
token1_amount: u64
token2_amount: u64
token1_to_currency_pyth_feed_address: Pubkey
token2_to_currency_pyth_feed_address: Pubkey
provider: u8
Given that we have 4 * 32 + 2 * 64 bytes + 8 bytes = 264 bytes, we can have around 20 pools that we can deposit at any given point in time (because of a 4KB account limit on solana)
The second option seems like the way to go, as the first one if off-chain and prone to oracle-attacks. However, the second option still seems a bit hacky, as I would have to include this data-structure anytime I intend to calculate the total TVL.
Is there any other design ideas that come to your mind or that you have seen, that would be appropriate?
I don't know much about the overall design of your program to provide you with a good solution. I also don't know what invariant is, maybe that breaks what I'm about to describe below.
I assume that you have some instruction in your program which cpi calls into Saber etc and opens a liquidity position. Assuming that that instruction creates an account on the chain with the following information:
pool_address,
token_1_mint_address
token_2_mint_address
amount_token_1
amount_token_2
...
One simple solution is to loop through all those accounts, and since you have the amount and mint of each token, you can calculate the value using something like the pyth price oracle. I wouldn't do this on chain though since it can become pretty expensive fast! Perhaps is best to do it on the client side and write this information back to the chain.
The recent solana bootcamp videos actually have a tutorial on bringing off chain info back to the chain! https://www.youtube.com/watch?v=GwhRWde3Ckw&t=385s
Below is a demo of the runtime limitations of on chain programs, perhaps you could do the loop through the pool accounts, if you use some indexing and PDA to find the account address and assuming that you have a limited number of liquidity positions! However I wouldn't hardcode all the information into a single account, seems like an unsustainable approach that might cause a lot of issues down the line. Might give you superior performance however, not sure.
https://www.youtube.com/watch?v=5IrfSecDPeA&t=1191s
Anyways GL!

How concerned should we be about brute force attacks when creating passwords?

Are brute force attacks effective in the real world? Almost every service I have ever used where a password was needed had some type of protection against trying even more than a few passwords. Also, how does this translate to data encryption?
brute force a.k.a. exhaustive key search - is a method that can work on multiple attack vectors
what you describe is attacking a service by authenticating through the service. The service here acts as a gate keeper, and can prevent the attack by limiting your attempts, etc.
However that is only one attack vector to the system:
what if an attacker gains access to the user database by other means? will he be able to leverage this knowledge to access the service now?
if the owner of said service was dumb enough to store clear text passwords ... ouch... but let's assume the passwords are stored in a way that uses at least a one way function like a hashfunction
let's see what changed...
we can have as many attempts on the password guessing side as our available computational ressources allow ...
the only remaining gatekeeper here is the complexity of guessing a password
if we only need to check value == hash(password) we can easily see that there are other ways of efficiently attacking this than brute force ...
but you asked for brute force specifically so let's assume our passwords are salted...
value == hash(password,salt) ... no more rainbow tables... but this is still bad, since we can possibly calculate that very quickly and parallelize ... the limiting factor that remains is hardware ... if we throw in a number of (rented? ... hacked?) number crunching systems with enough cores / GPUs ... how fast can we crack a password by brute force? ... let's do a little math here ...
let's say we have one system that does our work ... doing roughly 1,000,000 password guesses per second ... let's say our password has an entropy of ~42 bit ... that's roughly a 9 letter lowercase password ... how long does it take?
~51 days untill a guaranteed positive guess if the attacker knows the allowed char pool (lower-alpha in our case)
you can assume that after 50% of that time the probability of a correct guess is 50% so... not very long...
but it get's worse ... we assumed one machine ... does it scale if we buy/rent/hack more? sure it does ... let's assume 10,000 machines ...
we can simply divide by 10k ... and we are down to roughly 7 minutes 21 seconds ...
what does it cost to rent 10k machines from a cloud provider for 10 minutes? ... as an example (we didn't specify the complexity of our hash function so this example might not be in the range of those guesses per second, but you will see where this goes) ... an a1.xlarge amazon ec2 on-demand instance costs roughly 10 us-cents per hour ... we need it for ... 10 min ... so divide by 6 ... we want 10k nodes ... round about 170 USD ... let's round up ... 200 USD as a rough estimate
so... computational ressources have become available in vast numbers for relatively small money if we wanted to rent them... now think about malware capturing computational ressources
so ... with all that computational ressources on the bad guy side ... what can we do?
we can make guessing harder ...
if we increase the password complexity from those 42 bit above to ... lets say lower and upper alphanumeric and a length of 16 chars ... that's an entropy of roughly 96 bit ... and our little machine with 1,000,000 guesses per second will take roughly 2.5 quadrillion years to find our password (guaranteed... if it does not malfunction, has enough power, and you find a way to make it survive the end of our sun in a few billion years... but that's another story)
but back to the real world .. we take another cloud setup that can guess 100,000,000,000,000 passwords per second ... 10k times the size of the 10k machines we calculated earlier with... it will take us 25 million years to have a guaranteed hit ... probable success after 50% of that time ... so why does that not counter the increase of those additional 54bit entropy? ... exponential growth ... each bit more doubles the work...
another thing we can do besides the increase of our entropy is to increase the calculation complexity of a guess
if we chain our hash function like hash(hash(hash(...))) for a variable number of rounds, we can increase the time it takes to calculate the result ...
for a regular login, it does not really matter if your login takes 10ms or 250ms, but for an attacker, that difference means he has to afford 25 times the computational ressources or it takes 25 times as long... not as mighty as our entropy increase, but still... it's a measurable effect at very little cost on our side
when it comes to data encryption we usually don't talk about passwords but encryption keys ... a few bytes of entropy... at this level it's basically the same calculation as above ... at around 80 to 90 bits it's rather hard and expensive ... at around 40 ... whoopsy it's open ...
the interesting part is when you have to use a password to encrypt something ... how to get from the password to the needed bunch of key bytes? hashing? probably... but how?
the good thing is ... other folks had that very same problem before us ... and that resulted in well documented and examined functions like PBKDF2 (Password Derived Key Derivation Function ... you can kind of see it as a hash function with variable length output ... it creates a pseudo random bit stream and it's up to you to take as many bits as you need... typically the key size of a symetric cipher)
PBKDF2 has a round parameter to increase computation time ... so basically... it's the very same as above

How to protect against Replay Attacks

I am trying to figure out a way to implement decent crypto on a micro-controller project. I have an ARMv4 based MCU that will control my garage door and receive commands over a WiFi module.
The MCU will run a TCP/IP server, that will listen for commands from Android clients that can connect from anywhere on the Internet, which is why I need to implement crypto.
I understand how to use AES with shared secret key to properly encrypt traffic, but I am finding it difficult to deal with Replay Attacks. All solutions I see so far have serious drawbacks.
There are two fundamental problems which prevent me from using well
established methods like session tokens, timestamps or nonces:
The MCU has no reliable source of entropy, so I can't generate
quality random numbers.
The attacker can reset the MCU by cutting power to the garage,
thus erasing any stored state at will, and resetting time counter to
zero (or just wait 49 days until it loops).
With these restrictions in mind, I can see only one approach that seems
ok to me (i.e. I don't see how to break it yet). Unfortunately, this
requires non-volatile storage, which means writing to external flash,
which introduces some serious complexity due to a variety of technical details.
I would love to get some feedback on the following solution. Even better, is there a solution I am missing that does not require non-volatile storage?
Please note that the primary purpose of this project is education. I realize that I could simplify this problem by setting up a secure relay inside my firewall, and let that handle Internet traffic, instead of exposing the MCU directly. But what would be the fun in that? ;)
= Proposed Solution =
A pair of shared AES keys will be used. One key to turn a Nonce into an IV for the CBC stage, and another for encrypting the messages themselves:
Shared message Key
Shared IV_Key
Here's a picture of what I am doing:
https://www.youtube.com/watch?v=JNsUrOVQKpE#t=10m11s
1) Android takes current time in milliseconds (Ti) (64-bit long) and
uses it as a nonce input into the CBC stage to encrypt the command:
a) IV(i) = AES_ECB(IV_Key, Ti)
b) Ci = AES_CBC(Key, IV(i), COMMAND)
2) Android utilizes /dev/random to generate the IV_Response that the
MCU will use to answer current request.
3) Android sends [<Ti, IV_Response, Ci>, <== HMAC(K)]
4) MCU receives and verifies integrity using HMAC, so attacker can't
modify plain text Ti.
5) MCU checks that Ti > T(i-1) stored in flash. This ensures that
recorded messages can't be replayed.
6) MCU calculates IV(i) = AES_ECB(IV_Key, Ti) and decrypts Ci.
7) MCU responds using AES_CBC(Key, IV_Response, RESPONSE)
8) MCU stores Ti in external flash memory.
Does this work? Is there a simpler approach?
EDIT: It was already shown in comments that this approach is vulnerable to a Delayed Playback Attack. If the attacker records and blocks messages from reaching the MCU, then the messages can be played back at any later time and still be considered valid, so this algorithm is no good.
As suggested by #owlstead, a challenge/response system is likely required. Unless I can find a way around that, I think I need to do the following:
Port or implement a decent PRGA. (Any recommendations?)
Pre-compute a lot of random seed values for the PRGA. A new seed will be used for every MCU restart. Assuming 128-bit seeds, 16K of storage buys be a 1000 unique seeds, so the values won't loop until the MCU successfully uses at least one PRGA output value and restarts a 1000 times. That doesn't seem too bad.
Use the output of PRGA to generate the challenges.
Does that sound about right?
Having an IV_KEY is unnecessary. IVs (and similar constructs, such as salts) do not need to be encrypted, and if you look at image you linked to in the youtube video you'll see their description of the payload includes the IV in plaintext. They are used so that the same plaintext does not encode to the same ciphertext under the same key every time, which presents information to an attacker. The protection against the IV being altered is the HMAC on the message, not the encryption. As such, you can remove that requirement. EDIT: This paragraph is incorrect, see discussion below. As noted though, your approach described above will work fine.
Your system does have a flaw though, namely the IV_Response. I assume, from that you include it in your system, that it serves some purpose. However, because it is not in any way encoded, it allows an attacker to respond affirmatively to a device's request without the MCU receiving it. Let's say that your device's were instructing an MCU that was running a small robot to throw a ball. Your commands might look like.
1) Move to loc (x,y).
2) Lower anchor to secure bot table.
3) Throw ball
Our attacker can allow messages 1 and 3 to pass as expected, and block 2 from getting to the MCU while still responding affirmatively, causing our bot to be damaged when it tosses the ball without being anchored. This does have an imperfect solution. Append the response (which should fit into a single block) to the command so that it is encrypted as well, and have the MCU respond with AES_ECB(Key, Response), which the device will verify. As such, the attacker will not be able to forge (feasibly) a valid response. Note that as you consider /dev/random untrustworthy this could provide an attacker with plaintext-ciphertext pairs, which can be used for linear cryptanalysis of the key provided an attacker has a large set of pairs to work with. As such, you'll need to change the key with some regularity.
Other than that, your approach looks good. Just remember it is crucial that you use the stored Ti to protect against the replay attack, and not the MCU's clock. Otherwise you run into synchronization problems.

using counter instead of salt for hashing

I'm developing own protocol for secure message exchanging.
Each message contains the following fields: HMAC, time, salt, and message itself. HMAC is computed over all other fields using known secret key.
Protocol should protect against reply attack. On large time interval "time" record protects against replay attack (both sides should have synchronized clocks). But for protection against replay attack on short time intervals (clocks are not too accurate) I'm planning replace "salt" field with counter increasing every time, when new message is send. Receiving party will throw away messages with counter value less or equal to the previous message counter.
What I'm doing wrong?
Initial counter value can be different (I can use party identifier as initial value), but it will be known to the attacker (party identifier transmitted in unencrypted form).
(https://security.stackexchange.com/questions/8246/what-is-a-good-enough-salt-for-a-saltedhash)
But attacker can precompute rainbow tables for counter+1, counter+2, counter+3... if I will not use really random salt?
I'm not certain of your design and requirements, so some of this may be off base; hopefully some of it is also useful.
First, I'm having a little trouble understanding the attack; I'm probably just missing something. Alice sends a message to Bob that includes a counter, a payload, and an HMAC of (counter||payload). Eve intercepts and replays the message. Bob has seen that one, so he throws it away. Eve tries to compute a new message with counter+1, but she is unable to compute the HMAC for this message (since the counter is different), so Bob throws it away. As long as there is a secret available, Eve should never be able to forge a message, and replaying a message does nothing.
So what is the "known secret key?" Is this key known to the attacker? (And if it is, then he can trivially forge messages, so the HMAC isn't helpful.) Since you note that you have DH, are you using that to negotiate a key?
Assuming I'm missing the attack, thinking through the rest of your question: If you have a shared secret, why not use that to encrypt the message, or at least the time+counter? By encrypting the time and counter together, a rainbow table should be impractical.
If there is some shared secret, but you don't have the processor available to encrypt, you could still do something like MD5(secret+counter) to prevent an attacker guessing ahead (you must already have MD5 available for your HMAC-MD5).
I have attacked this problem before with no shared secret and no DH. In that case, the embedded device needed a per-device public/private keypair (ideally installed during manufacturing, but it can be computed during first power-on and stored in nonvolatile memory; randomness is hard, one option is to let the server provide a random number service; if you have any piece of unique non-public information on the chip, like a serial number, that can be used to seed your key, too. Worst case, you can use your MAC plus the time plus as much entropy as you can scrounge from the network.)
With a public/private key in place, rather than using HMAC, the device just signs its messages, sending its public key to the server in its first message. The public key becomes the identifier of the device. The nice thing about this approach is that there is no negotiation phase. The device can just start talking, and if the server has never heard of this public key, it creates a new record.
There's a small denial-of-service problem here, because attackers could fill your database with junk. The best solution to that is to generate the keys during manufacturing, and immediately insert the public keys into your database. That's impractical for some contract manufacturers. So you can resort to including a shared secret that the device can use to authenticate itself to the server the first time. That's weak, but probably sufficient for the vast majority of cases.

Why should checking a wrong password take longer than checking the right one?

This question has always troubled me.
On Linux, when asked for a password, if your input is the correct one, it checks right away, with almost no delay. But, on the other hand, if you type the wrong password, it takes longer to check. Why is that?
I observed this in all Linux distributions I've ever tried.
It's actually to prevent brute force attacks from trying millions of passwords per second. The idea is to limit how fast passwords can be checked and there are a number of rules that should be followed.
A successful user/password pair should succeed immediately.
There should be no discernible difference in reasons for failure that can be detected.
That last one is particularly important. It means no helpful messages like:
Your user name is correct but your password is wrong, please try again
or:
Sorry, password wasn't long enough
Not even a time difference in response between the "invalid user and password" and "valid user but invalid password" failure reasons.
Every failure should deliver exactly the same information, textual and otherwise.
Some systems take it even further, increasing the delay with each failure, or only allowing three failures then having a massive delay before allowing a retry.
This makes it take longer to guess passwords.
I am not sure, but it is quite common to integrate a delay after entering a wrong password to make attacks harder. This makes a attack practicaly infeasible, because it will take you a long time to check only a few passwords.
Even trying a few passwords - birthdates, the name of the cat, and things like that - is turned into no fun.
Basically to mitigate against brute force and dictionary attacks.
From The Linux-PAM Application Developer's Guide:
Planning for delays
extern int pam_fail_delay(pam_handle_t *pamh, unsigned int micro_sec);
This function is offered by Linux-PAM
to facilitate time delays following a
failed call to pam_authenticate() and
before control is returned to the
application. When using this function
the application programmer should
check if it is available with,
#ifdef PAM_FAIL_DELAY
....
#endif /* PAM_FAIL_DELAY */
Generally, an application requests
that a user is authenticated by
Linux-PAM through a call to
pam_authenticate() or pam_chauthtok().
These functions call each of the
stacked authentication modules listed
in the relevant Linux-PAM
configuration file. As directed by
this file, one of more of the modules
may fail causing the pam_...() call to
return an error. It is desirable for
there to also be a pause before the
application continues. The principal
reason for such a delay is security: a
delay acts to discourage brute force
dictionary attacks primarily, but also
helps hinder timed (covert channel)
attacks.
It's a very simple, virtually effortless way to greatly increase security. Consider:
System A has no delay. An attacker has a program that creates username/password combinations. At a rate of thousands of attempts per minute, it takes only a few hours to try every combination and record all successful logins.
System B generates a 5-second delay after each incorrect guess. The attacker's efficiency has been reduced to 12 attempts per minute, effectively crippling the brute-force attack. Instead of hours, it can take months to find a valid login. If hackers were that patient, they'd go legit. :-)
Failed authentification delays are there to reduce the rate of login attempt. The idea that if somebody is trying a dictionary or a brute force attack against one or may user accounts that attacker will be required to wait the fail delay and thus forcing him to take more time and giving you more chance to detect it.
You might also be interested in knowing that, depending on what you are using as a login shell there is usually a way to configure this delay.
In GDM, the delay is set in the gdm.conf file (usually in /etc/gdm/gdm.conf). you need to set RetryDelay=x where x is a value in seconds.
Most linux distribution these day also support having FAIL_DELAY defined in /etc/login.defs allowing you to set a wait time after a failed login attempt.
Finally, PAM also allows you to set a nodelay attribute on your auth line to bypass the fail delay. (Here's an article on PAM and linux)
I don't see that it can be as simple as the responses suggest.
If response to a correct password is (some value of) immediate, don't you only have to wait until longer than that value to know the password is wrong? (at least know probabilistically, which is fine for cracking purposes) And anyway you'd be running this attack in parallel... is this all one big DoS welcome mat?
What I tried before appeared to work, but actually did not; if you care you must review the wiki edit history...
What does work (for me) is, to both lower the value of pam_faildelay.so delay=X in /etc/pam.d/login (I lowered it to 500000, half a second), and also add nodelay (preceded by a space) to the end of the line in common-auth, as described by Gabriel in his answer.
auth [success=1 default=ignore] pam_unix.so nullok_secure nodelay
At least for me (debian sid), only making one of these changes will not shorten the delay appreciably below the default 3 seconds, although it is possible to lengthen the delay by only changing the value in /etc/pam.d/login.
This kind of crap is enough to make a grown man cry!
On Ubuntu 9.10, and I think new versions too, the file you're looking for is located on
/etc/pam.d/login
edit the line:
auth optional pam_faildelay.so delay=3000000
changing the number 3 with another you may want.
Note that to have a 'nodelay' authentication, I THINK you should edit the file
/etc/pam.d/common-auth
too. On the line:
auth [success=1 default=ignore] pam_unix.so nullok_secure
add 'nodelay' to the final (without quotes).
But this final explanation about the 'nodelay' is what I think.
I would like to add a note from a developers perspective. Though this wouldn't be obvious to the naked eye a smart developer would break out of a match query when the match is found. In witness, a successful match would complete faster than a failed match. Because, the matching function would compare the credentials to all known accounts until it finds the correct match. In other words, let's say there are 1,000,000 user accounts in order by IDs; 001, 002, 003 and so on. Your ID is 43,001. So, when you put in a correct username and password, the scan stops at 43,001 and logs you in. If your credentials are incorrect then it scans all 1,000,000 records. The difference in processing time on a dual core server might be in the milliseconds. On Windows Vista with 5 user accounts it would be in the nanoseconds.
I agree. This is an arbitrary programming decision. Putting the delay to one second instead of three doesn't really hurt the crackability of the password, but makes it more user-friendly.
Technically, this deliberate delay is to prevent attacks like the "Linearization attack" (there are other attacks and reasons as well).
To illustrate the attack, consider a program (without this
deliberate delay), which checks an entered serial to see whether it
matches the correct serial, which in this case happens to be
"xyba". For efficiency, the programmer decided to check one
character at a time and to exit as soon as an incorrect character is
found, before beginning the lengths are also checked.
The correct serial length will take longer to process than an incorrect serial length. Even better (for attacker), a serial number
that has the first character correct will take longer than any that
has an incorrect first character. The successive steps in waiting time
is because each time there's one more loop, comparison to go through
on correct input.
So, attacker can select a four-character string and that the string beginning with x takes the most time. (by guess work)
Attacker can then fix character as x and vary the second character, in which case they will find that y takes the longest.
Attacker can then fix the first two characters as xy and vary the third character, in which case they will find that b takes the
longest.
Attacker can then fix the first three character as xyb and vary the fourth character,in which case they will find that a takes the
longest.
Hence, the attackers can recover the serial one character at a time.
Linearization.java.
Linearization.docx, sample output
The serial number is four characters long ans each character has 128
possible values. Then there are 1284 = 228 =
268,435,456 possible serials. If attacker must randomly guess
complete serial numbers, she would guess the serial number in about
227 = 134,217,728 tries, which is an enormous amount of work. On the other hand, by using the linearization attack above, an
average of only 128/2 = 64 guesses are required for each letter, for a
total expected work of about 4 * 64 = 28 = 256 guesses,
which is a trivial amount of work.
Much of the written martial is adapted from this (taken from Mark Stamp's "Information Security: Principles and Practice"). Also the calculations above do not take into account the amount of guesswork needed to to figure out the correct serial length.

Resources