Reputation: 35275
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
Reputation: 2564
I analyzed your code and found the bugs:
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
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
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