2

Is there any documentation that exhaustively lists the optimizations that the Java JIT can make? I can easily find articles with examples of what the JIT can do, but I want to make sure that it's not going to optimize away a password hash comparison and expose me to a timing attack.

My code is loosely:

Record record = SELECT user.hash, true as is_real FROM user 
                WHERE user.email = :email:
                UNION SELECT :dummy_hash:, false
                ORDER BY is_real desc
                LIMIT 1;
byte[] hash = record.getValue1();
boolean real = record.getValue2();
boolean hashCorrect = verify(plaintextPassword, hash);
return hashCorrect && real;

I want to make sure that Java spends the time comparing the password to my dummy-hash, so an attacker can't enumerate a list of my users' email addresses by attempting to log in with a bad password, and seeing which ones return a 403 FORBIDDEN more quickly.

  • First off: I don't know of any Java JIT. All the JIT compilers in the Java ecosystem compile JVM bytecode language, not Java. Secondly, which JIT? Oracle's? (Which of the 5 or so?) IBM's? Azul's? – Jörg W Mittag May 19 '18 at 0:41
  • Second off: if you write a performance test the JIT you use will show you how much time it takes. – candied_orange May 19 '18 at 1:10
  • 1
    Any exhaustively lists of the optimizations that the Java JIT can make will be obsolete as soon as a new version is out. Futhermore, that is not really your problem, what you need is to "make sure that Java spends the time comparing the password to my dummy-hash" not to know all the possible optimizations. In general, what you do for that, is to convert the hash to their binary representation and compare them with bitwise operations (skipping bool operations and skipping string comparison), write code that compare the whole data and does not short cut, to go the extra mile, use memory berriers – Theraot May 19 '18 at 4:26
3

No, there no upper bound on the optimizations a JVM is allowed to perform. Therefore, it is not fundamentally possible to prevent optimizations. Instead, strongly prefer using functions from the Java standard library, as they may be able to offer additional security guarantees on the JVM they were designed for.

In your particular example, verify() cannot be optimized away entirely if it performs side effects, or if all data is somehow included in the return value. E.g. returning an integer where some bits indicate the verification result force that computation to be performed. However, this cannot prevent optimizations as your function might be inlined.

If constant-time operations are not possible, consider that random sleeping may also be a sufficient defense. By increasing the variance of the response time for both cases so that the timing distributions overlap, attackers would need an excessively high number of attempts per email to label them as known/unknown with sufficient confidence. The goal here is not to make timing attacks impossible, but to make them so expensive that logging or intrusion detection will notice the attack, and can respond e.g. by rate limits, IP bans, user notifications, or other mechanisms.

| improve this answer | |
2

No conforming JVM can perform an optimization that would cause the order of side effects as specified in the language definition to change. Any other optimization is available to it.

In your particular case, it would not be permissible for the hash verification to be optimized away if there is an observable side effect of testing the 'real' result. You could, for instance, achieve this like this:

in some global state somewhere, add this:

volatile AtomicBoolean lastTest;

then change your code to:

byte[] hash = record.getValue1();
AtomicBoolean real = new AtomicBoolean(record.getValue2());
lastTest = real;   // by making this assignment, the optimizer is no longer allowed 
                   // to assume that other threads cannot access or modify 'real',
                   // so all reads and writes must actually be performed

boolean hashCorrect = verify(plaintextPassword, hash);
return hashCorrect && real.getAndSet(false);

the VM cannot then prevent the hash verification taking place because by doing so it might cause a side effect happening that could potentially be observable in another thread and which the language definition states shouldn't happen.

(Of course, I assume that there is no information available to the compiler that would allow it to know the result of the verify call without having to actually call it... so the plaintext password and hash both come from external sources and the hash algorithm is complex enough that it cannot be statically analysed in advance to discover what combinations of parameters give known results ... but in any realistic system of this kind this is definitely true)

| improve this answer | |
1

You are doing it with needless complexity.

Firstly, you have a complex SQL query, which should be avoided. Your business logic should not be written in SQL. I would use SQL just for queries and implement the logic in Java. Essentially, your code looks like you are unwilling to implement the logic in Java, and thus do SQL changes to maintain the Java code as unmodified as possible. Why is that the case?

Secondly, you are wasting precious CPU cycles.

Add a random sleep with suitably defined approximate time interval, approximately the same time it would take to verify the hash. Do this if the user isn't found. This makes it impossible for the client to get timing information, and also saves precious CPU cycles by genuinely sleeping instead of spin-looping. You may want to add some randomness common to the "not found" and "found" code paths as well to make it even more secure, to prevent people from logging in too fast.

How the randomness should be done? For the "not found" code path, get the standard deviation and mean of your password hash calculation times for lots of samples (e.g. 1000), and then approximate this with a Gaussian distribution. For the common code path, you should add more standard deviation than the "not found" code path has.

Also, you could take the role of the attacker and try to do statistical analysis for how hard it's via timing information possible to verify whether one email address is there. If it takes more than a minute, I'm sure nobody will enumerate your email addresses, but just attack isolated ones. If it takes more than an hour, even isolated ones won't probably be queried.

Also, you might want to somehow limit requests coming from a single IP address. I would use the token bucket algorithm for this to allow login bursts, but to restrict the rate of logins in the long term. This would also make it harder to enumerate the e-mail addresses or even query isolated ones.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.