Bruce
Bruce

Reputation: 35275

SMTP AUTH using CRAM-MD5

Following the guideline given in SMTP with CRAM-MD5 in Java I wrote a small program in Python to calculate the response when given the nonce as input:

import hashlib
from base64 import b64encode, b64decode 
import sys
from decimal import *

#MD5(('secret' XOR opad), MD5(('secret' XOR ipad), challenge))
#opad - 0x5C, ipad - 0x36.

def main(nonce):
   pwd = bytearray("password")

   for i in range(len(pwd)):
       pwd[i] = pwd[i] ^ 0x36

   m1 = hashlib.md5()
   m1.update(pwd.decode())
   m1.update(b64decode(nonce))

   m2 = hashlib.md5()

   pwd = bytearray("password")

   for i in range(len(pwd)):
       pwd[i] = pwd[i] ^ 0x5C

   m2.update(pwd.decode())
   m2.update(m1.hexdigest())


   print b64encode("username " + m2.hexdigest())


if __name__ == "__main__":
   if (len(sys.argv) != 2):
      print("ERROR usage: smtp-cram-md5 <nonce>")
   else:
     main(sys.argv[1])                

However, the SMTP server rejects the response I give generated by this program. Can some one please point out what I am doing wrong?

Upvotes: 1

Views: 3165

Answers (3)

phobie
phobie

Reputation: 2564

I analyzed your code and found the bugs:

  1. You don't just need to xor your password but all 64-bytes
  2. The key should not be decoded since md5.update() works with binary data
  3. For the same reason you need to call m1.digest() instead of m1.hexdigest()

Your code with my fixes and py3k compatibility:

import hashlib
from base64 import b64encode, b64decode 
import sys

def main(nonce):
    pwd = bytearray('password'.encode('utf-8'))

    key = bytearray(64*b'\x36')

    for i in range(len(pwd)):
        key[i] ^= pwd[i]

    m1 = hashlib.md5()
    m1.update(key)
    m1.update(b64decode(nonce))

    m2 = hashlib.md5()

    key = bytearray(64*b'\x5c')

    for i in range(len(pwd)):
        key[i] ^= pwd[i]

    m2.update(key)
    m2.update(m1.digest())

    response = "username " + m2.hexdigest()
    print(b64encode(response.encode('utf-8')).decode('ascii'))

if __name__ == "__main__":
    if (len(sys.argv) != 2):
        print("ERROR usage: smtp-cram-md5 <nonce>")
    else:
        main(sys.argv[1])

Disclaimer: This code is only valid for password length up to 64 bytes! (See RFC 2195)

Upvotes: 0

phobie
phobie

Reputation: 2564

A example implementation of CRAM-MD5 with HMAC. Tested with python2.7 and python3.4. On Python 3 the hashlib import can be avoided by replacing hashlib.md5 with 'md5'.

"""
doc-testing with example values from RFC 2195

>>> challenge = 'PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+'
>>> user = 'tim'
>>> password = 'tanstaaftanstaaf'
>>> target_response = 'dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw'
>>> actual_response = cram_md5(user, password, challenge)
>>> target_response == actual_response
True
"""

import base64
import hashlib
import hmac

def cram_md5(user, password, challenge):
    password = password.encode('utf-8')
    challenge = base64.b64decode(challenge)
    digest = hmac.HMAC(password, challenge, hashlib.md5).hexdigest()
    response = '{} {}'.format(user, digest).encode()
    return base64.b64encode(response).decode()

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Upvotes: 2

Max
Max

Reputation: 10985

You can use the hmac module to calculate this, or at least to double check your output.

Are you using Python2.x or 3.x? You may have some bytes/strings issues, as well.

Specifically, after byte munging pwd.decode() is likely to give you garbage, as it tries to make sense of stuff that's no longer character data.

You also appear to be missing the step to extend the key block to a multiple of the input block size of the hash function.

The wikipedia article for HMAC includes a small example in Python that might be useful.

Upvotes: 1

Related Questions