Commit a41f9c8c authored by Colomban Wendling's avatar Colomban Wendling

Add support for FreeBSD (DES-ECB) to the Python tool

Closes #3.
parent 01ad50fc
......@@ -34,11 +34,6 @@ and ClawsMail is written in C so I could reuse their routine without caring
further about what it actually does, but re-did it in Python when thinking
about releasing it because I guess it'll be easier to use for you.
Unfortunately, I wasn't able to find how to decrypt the DES-ECB used under
FreeBSD with Python, because it needs to decrypt blocks of sizes not multiple
of 8, which doesn't seem really possible -- though ClawsMail does it. I'm all
ears if you find me a solution here. But for now, for the FreeBSD users I also
release the C version that uses the actual ClawsMail code so works everywhere.
Python tool
-----------
......@@ -66,6 +61,9 @@ Note that DES requires a key. By default ClawsMail uses `passkey0` as the key,
but this can be changed at build time. The tool uses this as the default, but
supports the `-k <key>` switch if you need to use another one.
To decrypt passwords from a FreeBSD installation, use the `--freebsd` option,
which is an alias of the `--mode=ECB` option.
C tool
------
......@@ -96,3 +94,17 @@ don't build on FreeBSD, you need to define the `NEED_DES_ECB` C preprocessor
constant. You can use::
make CLAWSMAIL_SRC=/path/to/claws-mail/src CFLAGS='-DNEED_DES_ECB'
Note on encryption under FreeBSD (DES-ECB)
------------------------------------------
As mentioned above, for some reason ClawsMail encrypts passwords using DES-ECB
instead of DES-CFB under FreeBSD.
DES-ECB doesn't allow encrypted blocks of sizes not multiple of 8, and
ClawsMail doesn't introduce padding nor checks the encryption actually
succeeded. This means that password encryption under FreeBSD will fail
silently if the password length (in bytes) is not multiple of 8, leading to the
encryption phase simply being a no-op, meaning the password will only be
transformed through Base64. This sounds bad, but it's how it is.
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
# Copyright (C) 2013 Colomban Wendling <ban@herbesfolles.org>
# Copyright (C) 2013-2016 Colomban Wendling <ban@herbesfolles.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
......@@ -38,25 +38,22 @@ from ConfigParser import ConfigParser
PASSCRYPT_KEY = b'passkey0'
def pass_decrypt(p, key=PASSCRYPT_KEY):
"""
Decrypts a password from ClawsMail. This doesn't work for password stored
under FreeBSD because then ClawsMail uses DES ECB mode, but apparently
feeds it data of non-modulo 8 size. This looks impossible, but apparently
it works. I checked the implementation of ecb_crypt() from the glibc, and
it seems to actually not accept length non-modulo 8, although the ClawsMail
code DOES work with it:
des_setparity(des_key);
ecb_crypt(des_key, password, len, DES_ENCRYPT);
even when then length is clearly non-modulo 8. I have no idea hot to deal
with this then, so this won't work for FreeBSD users -- sorry guys. They
will either have to use a C implementation or find how to decrypt this.
"""
def pass_decrypt(p, key=PASSCRYPT_KEY, mode=DES.MODE_CFB):
""" Decrypts a password from ClawsMail. """
if p[0] == '!': # encrypted password
c = DES.new(key, mode=DES.MODE_CFB, IV=b'\0'*8)
return c.decrypt(b64decode(p[1:]))
buf = b64decode(p[1:])
"""
If mode is ECB or CBC and the length of the data is wrong, do nothing
as would the libc algorithms (as they fail early). Yes, this means the
password wasn't actually encrypted but only base64-ed.
"""
if (mode in (DES.MODE_ECB, DES.MODE_CBC)) and ((len(buf) % 8) != 0 or
len(buf) > 8192):
return buf
c = DES.new(key, mode=mode, IV=b'\0'*8)
return c.decrypt(buf)
else: # raw password
return p
......@@ -83,7 +80,13 @@ def accountrc_decrypt(filename, key=PASSCRYPT_KEY):
if __name__ == '__main__':
import os
from optparse import OptionParser
from optparse import OptionParser, OptionValueError
def mode_callback(option, opt, value, parser):
try:
parser.values.mode = getattr(DES, 'MODE_'+value.upper())
except AttributeError:
raise OptionValueError('Invalid mode "%s"' % value)
usage = 'Usage: %prog [OPTIONS] ENCRYPTED_PASS1...|FILE...'
parser = OptionParser(usage=usage)
......@@ -91,6 +94,16 @@ if __name__ == '__main__':
help='Use KEY to decode passwords (8 byte string) '
'[%default]',
metavar='KEY')
parser.add_option('-m', '--mode', dest='mode', default=DES.MODE_CFB,
type='string', action='callback', callback=mode_callback,
help='Use MODE to decrypt DES passwords. Choose ECB to '
'decrypt passwords from FreeBSD installations, CFB '
'otherwise [CFB]',
metavar='MODE')
parser.add_option('--freebsd', dest='mode',
action='store_const', const=DES.MODE_ECB,
help='Use ECB mode as needed for passwords from FreeBSD '
'(alias for --mode=ECB)')
(options, args) = parser.parse_args()
if len(args) < 1:
......@@ -100,5 +113,5 @@ if __name__ == '__main__':
if os.path.exists(a):
accountrc_decrypt(a, key=options.key)
else:
password = pass_decrypt(a, key=options.key)
password = pass_decrypt(a, key=options.key, mode=options.mode)
print('password "%s" is "%s"' % (a, password))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment