1
$\begingroup$

I came upon an idea the other day that I had some questions about. I am not a security professional but I do study with an amateur interest and have not heard of this particular idea before.

There were a few similar questions here, but :

Basically the idea is:

  • presume the existence of a pre shared secret between client and server of appropriate size
  • the server can send secret data to the client by sending a hash of the secret data prefixed to the shared secret.
  • amount of data sent per hash is small, a handful of bytes (i.e. a key fragment)
  • The client receives the hash. We can assume an attacker does as well
  • The length of the hash input material is length(secret_data) + length(shared_secret)
  • The client begins cracking the hash. The client must crack the first length(secret_data) bytes of data, as length(shared_secret) bytes of the input are already known.
  • Cracking a few bytes should be trivial. Time taken can be made to scale with the quantity of secret bytes sent.
  • Attacker must crack length(secret_data) + length(shared_secret) to recover the secret data. This should be infeasible.

The above is the idea in it's most simple form.

The following is a python implementation with slightly more detail, which I will mention afterwards:

""" Provides a simple protocol for establishing a new shared secret from an old one.
    This protocol is only valid for models that utilize initial registration through
    a secured channel (i.e. tls) or better, an out of band communication. """
import random
import itertools
import hashlib

# generate and establish an initial secret, for example via:
_initial_value = random._urandom(32)
# ... skipping how the exchange actually occurs, for brevity
# the initial_value MUST be transmitted securely (i.e. via tls or similar) or out of band!

# assuming both client and server have knowledge of the secret initial_value
def get_challenge(secret, key_size=32, bytes_per_hash=1, hash_function="sha256"):
    """ Usage: get_challenge(secret, key_size=32, 
                                   bytes_per_hash=1, 
                                   hash_function="sha256") => key, challenge, new_secret
        Create a challenge that only the holder of the shared secret (i.e. the client) can
        answer. Returns a randomly generated shared secret of key_size, and the challenge 
        with a message authentication code prefixed.

        Increasing the bytes_per_hash will increase the time required to
        crack a single hash exponentially. 

        Increasing key_size will increase the time required to complete the
        challenge by a multiple of how long it takes to crack a single hash"""          
    # on login:
    #    generate x random bytes and append the secret + the previous hash (or len(digest) null bytes for the first hash)
    #    hash the resultant string of length x + len(secret), buffer the bytes
    #    repeat the above, appending new hashes to the buffer while
    #    ensuring to hash the secret at each iteration
    #    send hashes to client
    challenge = key = ''
    hash_function = getattr(hashlib, hash_function)
    hash_output = '\x00' * 32

    # below is just for simplicity sake; DO NOT use the secret as the mac key directly
    # use hkdf to derive a mac key from the secret
    mac_key = secret 
    while len(key) < key_size:
        random_bytes = random._urandom(bytes_per_hash)
        key += random_bytes
        hash_input = key + secret + hash_function(hash_output + ':' + secret).digest()
        hash_output = hash_function(hash_input).digest()
        challenge += hash_output
   #     print "Created challenge: ", hash_function(hash_input).digest()
        secret = hash_function(secret).digest()        
    mac = hash_function(challenge + mac_key).digest()
  #  print "Generated key: ", key
    return key, mac + challenge, secret

def solve_challenge(secret, challenge, key_size=32, bytes_per_hash=1, hash_size=32, hash_function="sha256"):
    # client begins cracking hashes:
    #    number of unknown bytes: x; number of known bytes len(secret)
    #    x bytes are cracked in trivial amount of time and returned as part of the key
    #    repeat until all hashes are cracked, ensuring to hash the secret each round
    #    hashes must be cracked in order
    hash_function = getattr(hashlib, hash_function)
    assert hash_function(challenge[32:] + secret).digest() == challenge[:32], "Message Authentication Code Invalid"
    challenge = challenge[32:] # remove the mac
    key = ''
    range_256 = [chr(x) for x in range(256)]
    previous_hash = hash_function("\x00" * 32 + ':' + secret).digest()

    while challenge:
        current_hash = challenge[:hash_size]
    #    print "Cracking challenge: ", current_hash
        for permutation in itertools.permutations(range_256, bytes_per_hash):
            key_guess = ''.join(permutation)
            #print "Guessing: ", key_guess
            hash_output = hash_function(key + key_guess + secret + previous_hash).digest()
            if hash_output == current_hash:
                key += key_guess
                new_key_length = len(key)
                secret = hash_function(secret).digest()
                previous_hash = hash_function(hash_output + ':' + secret).digest()                
                break          
        else:            
            raise ValueError("Unable to recover bytes from hash")        

        challenge = challenge[hash_size:]
    #print "Recovered key ", len(key)
    return key, secret

# assume attacker receives hashes also...
# attacker begins cracking hash:
#    number of unknown bytes: x + len(secret) per hash
#    theoretically cannot crack a single hash, assuming the security of the 
#    underlying hash function holds true
#    UNLESS the attacker observed or obtained the secret value!
#    the hashes must be cracked in order

if __name__ == "__main__":
    key, mac_challenge, server_secret = get_challenge(_initial_value)
    _key, client_secret = solve_challenge(_initial_value, mac_challenge)
    assert key == _key
    assert server_secret == client_secret    

In the above example, a random 32 byte key is generated and split into 16 hashed secret messages and sent to the client. This key would be used to derive session keys.

My first question: Is it correct that without the shared secret, assuming the strength of the underlying hash function, the secret data should be unrecoverable?

Second question: Is data manipulated this way implicitly authenticated and integrity assured without the need for an additional external mac? The example includes one, is it necessary? My guess is yes because you wouldn't want to "decrypt" anything before verifying the intergrity.

The input for each hash includes the cumulative key fragments obtained so far and the hashed output of the last hash + secret. I had the idea this would ensure the hashes must be cracked in order. Is this correct?

The secret used in each challenge is also hashed before the next challenge is generated. Does this help or harm anything or should it be omitted?

Another potential application I considered was rate limiting/proof of work (unrelated to key exchange). The ability of a client to make requests could be slowed by a desired factor by adjusting the amount of secret data to crack and using the secret data as a capability/session id to perform the desired request. This would shift the rate limiting from server to clients by throttling to the physical capability of the client to make requests. At least, that is my idea; Are there already better designed/implemented schemes, and if so, may I be pointed towards them?

Presuming there is no way to crack the hashes substantially faster then brute force, is the best advantage an adversary could do in terms of advantage usage of gpu's and fpga's?

I gathered that the general consensus from the other questions was basically not to use such a construct while more thoroughly researched ones are readily available. No surprises there. Since there's a shared secret already there's little reason not to properly encrypt further secrets.

$\endgroup$
  • $\begingroup$ I see a few problems: e.g. you leak information if the amount of key material is small. In that case you may run into collisions at least at the beginning of the key material. That would already be enough reason not to use this protocol. It's unclear to me why you would want to include a proof of work with a key derivation method. You could just create a challenge, calculate k = KDF(shared secret, OtherInfo and challenge), send and send a challenge with MAC(k, OtherInfo and challenge). That's the usual method to derive keys / validate keys during symmetric key exchange. $\endgroup$ – Maarten Bodewes Dec 4 '15 at 15:32
  • $\begingroup$ What is the threshold size for a key to qualify as "small"? The first input in the demo above uses a 2 bytes + 32 byte key + the 32 byte output from the hash. How might the 2 initial bytes be leaked without knowledge of the 64 other bytes used in the input? The proof of work idea is separate from key exchange. I hoped that actions such as login attempts could maybe be rate limited by making the client crack a hash of appropriate size to acquire the "session id" to allow them to perform the requested action. What is the purpose/source of "other info" in the challenge scheme you suggested? $\endgroup$ – Ella Rose Dec 4 '15 at 20:02
  • $\begingroup$ Oh, can you outline the attack that would recover the leaked information? "Anyone can design a scheme they can't break". I thought of of the scheme, so I need someone else to show me how to break it. $\endgroup$ – Ella Rose Dec 4 '15 at 20:31

Your Answer

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

Browse other questions tagged or ask your own question.