From c52d8c5834a7d5f0df207277f6143cbae39052f9 Mon Sep 17 00:00:00 2001 From: tomrittervg Date: Sun, 6 Jan 2013 21:03:13 -0500 Subject: [PATCH] committing the tagging attack demo --- demo.py | 136 ++++++++++ key1.seckey | 31 +++ key2.seckey | 31 +++ key3.seckey | 31 +++ message.msg | 690 +++++++++++++++++++++++++++++++++++++++++++++++ mixConfig.py | 121 +++++++++ mixHeader.py | 145 ++++++++++ mixKey.py | 441 ++++++++++++++++++++++++++++++ mixKeystore.py | 124 +++++++++ mixMath.py | 92 +++++++ mixMessage.py | 209 ++++++++++++++ mixPacketType.py | 14 + mixPayload.py | 130 +++++++++ 13 files changed, 2195 insertions(+) create mode 100644 demo.py create mode 100644 key1.seckey create mode 100644 key2.seckey create mode 100644 key3.seckey create mode 100644 message.msg create mode 100644 mixConfig.py create mode 100644 mixHeader.py create mode 100644 mixKey.py create mode 100644 mixKeystore.py create mode 100644 mixMath.py create mode 100644 mixMessage.py create mode 100644 mixPacketType.py create mode 100644 mixPayload.py diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..4877a9c --- /dev/null +++ b/demo.py @@ -0,0 +1,136 @@ +#!/usr/bin/python + +import hashlib, base64, binascii +import mixKeystore, mixMessage + +DoTaggingAttack = True + +# ======================================================================== +# Specify the keystore the rest of the code will use. Build it ourselves +# to stop it from going to the filesystem. +mixKeystore._mixKeyStore = mixKeystore.MixKeyStore() + +fk1 = open('key1.seckey', 'r') +mixKeystore._mixKeyStore.addKey(fk1.readlines(), "key1") +fk1.close() + +fk2 = open('key2.seckey', 'r') +mixKeystore._mixKeyStore.addKey(fk2.readlines(), "key2") +fk2.close() + +fk3 = open('key3.seckey', 'r') +mixKeystore._mixKeyStore.addKey(fk3.readlines(), "key3") +fk3.close() + +#Open the Message +fm = open('message.msg', 'r') +msg1_lines = fm.readlines() +fm.close() + +# ======================================================================== +print "Client sends message with a Path of Node1,Node2,Node3" +print " by pure luck (or unluck) Nodes 1 and 3 are attacker-controlled" +# ======================================================================== +print "=" * 70 +print "Received Message on Node 1, processing..." +#Process the message +msg1 = mixMessage.MixMessage(msg1_lines) + +#Decrypt the Message As Node 1 +msg1.decode() + +#Display the Decrypted & Decoded Intermediate Message + +print "Message recieved by Node 1, decrypted, and decoded:" +msg1.pprint() + +#Create the message that will be sent to the second node +msg2_lines = msg1.deliveryBody() + +if DoTaggingAttack: + print "+" * 70 + print "Performing Tagging Attack" + print "+" * 70 + #We want to flip the 240th byte of the second Mix Header + #First seperate the message into it's components: + headerIndex = msg2_lines.index("-----BEGIN REMAILER MESSAGE-----") + lengthIndex = headerIndex + len("-----BEGIN REMAILER MESSAGE-----") + 1 + digestIndex = lengthIndex + len("20480") + 1 + dataIndex = digestIndex + len(base64.b64encode(hashlib.md5("").digest())) + 1 + footIndex = msg2_lines.index("-----END REMAILER MESSAGE-----") + + #Isolate the data + tampereddata = msg2_lines[dataIndex:footIndex].replace("\n", "") + tampereddata = base64.b64decode(tampereddata) + + #Corrupt the target byte (the actual mode of corruption is not significant) + # 512 bytes to get past the first Mix Header, then 240 bytes beyond that + targetByte = 240 + oldLength = len(tampereddata) + if tampereddata[512 + targetByte] == '\x00': + tampereddata = tampereddata[:512 + targetByte] + '\x01' + tampereddata[512 + targetByte + 1:] + else: + tampereddata = tampereddata[:512 + targetByte] + '\x00' + tampereddata[512 + targetByte + 1:] + assert(oldLength == len(tampereddata)) + + #Reassemble the message + from mixMath import splitToNPerLine + + output = "::" + "\n" + output += "Remailer-Type: tagging-attack-demo\n" + output += "\n" + output += "-----BEGIN REMAILER MESSAGE-----" + "\n" + output += "20480" + "\n" + output += base64.b64encode(hashlib.md5(tampereddata).digest()) + "\n" + tampereddata = base64.b64encode(tampereddata) + output += splitToNPerLine(tampereddata) + "\n" + output += "-----END REMAILER MESSAGE-----" + "\n" + + msg2_lines = output + +print "Sending Message on to Node 2..." +# ======================================================================== +print "=" * 70 +print "Received Message on Node 2, processing..." +#Process the message +msg2 = mixMessage.MixMessage(msg2_lines) + +#Decrypt the Message As Node 2 +msg2.decode() + +#Display the Decrypted & Decoded Intermediate Message +print "Message recieved by Node 2, decrypted, and decoded:" +msg2.pprint() + +#Create the message that will be sent to the second node +msg3_lines = msg2.deliveryBody() + +print "Sending Message on to Node 3..." +# ======================================================================== +print "=" * 70 +print "Received Message on Node 3, processing..." +#Process the message +msg3 = mixMessage.MixMessage(msg3_lines) + +#Decrypt the Message As Node 3 +try: + msg3.decode() +except Exception, e: + print "+" * 70 + print "Caught a Decoding Exception! Continuing Anyway..." + + msg3.decode(ignoreDigestErrors=True) + + firstHeader = msg3.Headers[0] + actualDigest = hashlib.md5( firstHeader.EncHeader_Decrypted[0:firstHeader.DecryptedHeader.byteIndex] ).digest() + observedDigest = firstHeader.DecryptedHeader.Digest + print "Actual Digest ", binascii.hexlify(actualDigest) + print "Included Digest", binascii.hexlify(observedDigest) + print " |______________||______________|" + print " Matches Corrupted " + print "+" * 70 + + +#Display the Decrypted & Decoded Intermediate Message +print "Message recieved by Node 3, decrypted, and decoded:" +msg3.pprint() \ No newline at end of file diff --git a/key1.seckey b/key1.seckey new file mode 100644 index 0000000..8178d96 --- /dev/null +++ b/key1.seckey @@ -0,0 +1,31 @@ +-----Begin Mix Key----- +Created: 2012-10-24 +Expires: 2013-10-24 +72f00ecf4f4e3af64d19772d4dd7d620 +0 +PODmouviE9Q= +HMWtMHP3cVxrXHQFz5XwIe78ToBXXAQ/oyWaLpt0 +2KB5b1hVc9tYYkUCE4OfPHuGEcF4z5aUUfPYtac7 +JtjvEB+IMFWLnSiME9/SKm9dHqiunO23gUIrt7II +al/+NQpijNqhw55RWlj6HGJloPqoStsme+dgLw05 +z6fEeGAvs5bJaiRFY9H7p+mq/BbpavCqaKMjmn5w +j00UKneDybn9d6RYtqO6SyTqWpFlfeIgdj+k4jkV +vP6p7ILNqIPAAhrbFsuO23N22y35nrJmzTED+4oB +pMxqIoyFFyWgDqtdi+XNCOlqW72w7SfxiMCEYYqt +Ttkk4mAZ/mjOxlN6+CExmDsXrDOJ8HKwtt/ojnMK +WvWhkmFs4JC0wGgwK8hrW30sBc+zxzU1OdWxTZNJ +j3qZ0x844eLuMYZlWxgMbwoRSXZwNqQxNokBJJv7 +lfTdneJdSsXJOdLnsXvOFcDxSdaPKMsluvFEqHay +JoGowwqrBwZOYETswlE3yg0WB6NHz2/JXa5tayta +9AI1VZCLW0p3CNXERE/CpQDjqVehEf/wHxTau+fO +9VH28CMq2CKqy9nUhhoTqT0v7yJrVBqPUkqpWBgp +z7xjLazobUTjpa9bmwAR2Gy32Cr69RH+iAnItiui +NyDe37ghLHzgR8e9VZu7rOQGxYBr3qzW2CwuDm35 +BlHCn6k0F29KxIRPzGgJfOOjIeS9ok8/ozPOpqdd +8fKvWCgSEko/Asa0NxQSGlVKEOeg/72HpHsj2j9g +w063OHOzDTTmCxjjMdENWsrfCOsjfG70T/r33Pg5 +pwGOErF79NmaDpnuUVjEjK0fohOgGO1/++hH4wFr +RpMkSoMFaAxAD80YL7PxSM59x2bWnEZby/U5S12/ +RvTFV8rVPZF+RiUFxeNYs+dMTSZMyBCrQxDeHQ81 +zlkJ18tV1jRM3uZMIlCBpcu1gbpstg== +-----End Mix Key----- diff --git a/key2.seckey b/key2.seckey new file mode 100644 index 0000000..6425f9e --- /dev/null +++ b/key2.seckey @@ -0,0 +1,31 @@ +-----Begin Mix Key----- +Created: 2012-10-24 +Expires: 2013-10-24 +24a17d807994cffbe65fdc6ce13d3562 +0 +/Ioo6UfgB9Y= +7EE23tSYGx9LG7CDB8KZo+uzGdT16XXUwIwpjoSI +n03huEZat7h309YcaamttZptgnFGzKIhwGRzgL8n +lU0vXkY+T4262kjpYdSk+vY2IqKSNW3NxnFUMlRw +qfcj97gQavP+PFOkP9Bzv4NpBD8vXVP2UqPVkOsZ +SsnJ4eX3pCJQwcE83oFaBvWDDfLN+rFop/04u+Ii +SpNBEIKUCWhrl5KIoR+4UTLbR3R0jHf8Llzo1v+t +6ajfnLHZobY7I8204cwTv+7vlMDCEfXOlN5BQnue +gSQcEAdS4ADmfsArPaQcHNz+y7NVNaXI5H4+Gb0A +iJeR+InKfhZMYyE2yXeNdHTAjGFPOLZi2WyXa7xx +FJG1PG2/x0qenbspxaaWUeUx/ThbrodssHvTiP/4 +tt6Q9ThSGD3ioJzy3GyBfiibpcaplxG+Un3Nxxoq +QtUDgJzflZMKuB7VykOJe5PU6dLd8DawfcO5cc6g +YeujCRqaL8ZNq0VOACbk+TgT3SE1pk+B+UPDUgjg +lBJsd+acuDNjVLj4DPen9te4qQx9usUCewXcBjDO +zwSxzctONzsOEYxb/Pjvb7+ANMWhvdzvH09xF7VG +xOtGUor62FDh0GwLNCZgBULuYsswrkU4hfH2HmPt +8sn6CD1BaqhmQlDHimOvIarRNc+eqRjBFtJVs9lm +y+v+yT4ncxA6Z4iXXsu4xU4KIefw7Sh0iZ9dMhQd +77SO469WPcR8om/tqGRM3R2SjFPOEDzA+YAJCysa +vLu0hf/9cf64fngaDaKmrholGwhpOcJi9u0+WClv +4r8q/bfPuVpKncZJWQJUXvT4YEKMqxeb6o6GoB6P +s5sfRQ7QamIzS/jqrwOihvuyw622XosmBd8E06bW +OpSesFbTBymHi/pODv0C2Re9ljukeE6IZaj52Mq1 +7yA2L/u33f7WGAFd2hk9VafzmH7VbQ== +-----End Mix Key----- diff --git a/key3.seckey b/key3.seckey new file mode 100644 index 0000000..55b77de --- /dev/null +++ b/key3.seckey @@ -0,0 +1,31 @@ +-----Begin Mix Key----- +Created: 2012-10-24 +Expires: 2013-10-24 +f3372c7effb5887858460b7ed2faab91 +0 +BqukvVHoOw4= +nH+L/3unhjLOLLOGiBZoOd3QYL78WxZzSIA8GbAn +7Nl8tSyVp2hmzmaAjlcRL9RUTT9UrO9OloAzGub6 +FzomPxC8HwrFz8fnpvmzosFYjn0EeB1AFd+Mw2oL +Cm/QNsKdXj/nA84EqBa6A9fFWPkW3iMjxM+sqVFg +aW9Yajx1eWo4eQ79SICr0nvRurR2uPTmDbEid84u +QkYSFN7mD7y4pgZ/Wc2rMEu9SLQsyrafHKLbK9jR +6qc1aPPIt0vfkfF5y6nDadeghNi7E8gh7VG0yx42 +l7KIYGiPsMsiWQ/0U7GwW2Xy4QntY4x43QWwsONI +xaVMEYVkeb3aM+wU6TCxy3MsF8eIwZLrhPMadPO+ +AgxoKnjbT/0gpflyLeqcTw5XbZgnZiIXHihm1aTp +xFbZs6GTmYmXF/y0a33KslVKRhH4LXEKpxEsdUKD +yGYw38wyYa6FF+QacMAcIl3KNx4qPoiUurdefAgY +OsF8GtsX/bHmdCqU7F1/R4+LNr9JWIgXKyBZMX0x +w/ORo5hjKr5yC0VoQQ2ee2TRJ+99OONLmN1kI/DQ +JrvapS4l9bFMNUFVN/FK7Daz1pRHKv9R9qJ6mbZY +lnhikp4hoG8PTAI1j1A1FMncQotg/tMPVTVggHDB +Q2xM4gPVbHWwu9b9+kQ9ablp4DKmeOAMckKi7Xxt +YFR01vN3gXKl1Np6YQ5XHHxbWBrtCzcxhsKwIOCN +kao+/vhb7LNVq4ljsuLQirc+wnedU+vLKxIX+o9j +wyY6H9t1i+9ixqivBg8pA0ez69MfA6JXfxPOEinG +Mf4yuznAGA4CCcAtilDOnoNAzVCmQtQJxyx0Fpig +aUWpvxESSFxQUTFUpfeaQAkLke4H6C+wNtHpHwFV +AFvB9m7F+INXILsb7DOZkImhthad9zWfbFEEDkum +6qEvWeZKnTVpjPaTU/Akf7LO8pi2yQ== +-----End Mix Key----- diff --git a/message.msg b/message.msg new file mode 100644 index 0000000..248f6db --- /dev/null +++ b/message.msg @@ -0,0 +1,690 @@ +:: +Remailer-Type: Mixmaster 3.1-alpha2 + +-----BEGIN REMAILER MESSAGE----- +20480 +PVdM1MgKr0b/yxciKvosOg== +cvAOz09OOvZNGXctTdfWIIBQ06mCT2lpze6NBIdW +ppNMJAgBCDPl3fUpywoNZEUtn8UNykSWmTWInoyu +ZcCQi9eC/lPQg6b5/I78ZkVxD4B4zZaz97UQSLd6 +eKN3Hp/+rS1yuvvlGasaej7jfuuDQxbG/1dS4GmV +F6ykYGRLjKLsJlJ0vtcKocOm759HQVPYSgvxJ5ta +v/WPVqbpxOmbioCaKg3sCD/Pgjo27nQDAYVmJe7u +eKpiHxnTTQWFu+EfLkBa5PjKAK5tTg+LIOej8yhS +DBRnHraRXv2h6HepbdC2NngzspN11cV//ZoyjNKK +PcMQhfCW4tL2XVo/oyIMIDIUwv2oKIxVRUmOOAYb +7bC1JiOnhoIXqxsMWNMb/05qINixREIolGTU0vdF +J8s8BEhC6V+sKWTVOkoA+WGyzcn8xiG255OOnexB +jYJIeywSeXEzazaylsuJNO1g+OJFpT/jsA5xOJum +wmxXt4ebgRlsGPnJfZUllCiA0pZlBb4Tlc0ITRAA +s6oWFMWfGwVKmeYB8M4OoAaV9DYyMoNRSViCnOoT +tUEXJOL1kAulzvFLMUzV2MOEu+3dCEd58kIufbbB +LyJiMeO5hhupPwjAJufw7duNadb9NHskSOOIoBsC +u21p4Le2WbOBwmqlhRZ0gZdNf17a8x/mkjMahxUA +6TaoIymZ4QbEiIw3CuRpH0iU6F1tOnjxFcHw+Vlg +CPmY/qXdj+YqFx+7kBf5Q5LBBDyNmU2pOAwudSup +iwFfSWWI91mjQhEc9FXDSZt7SiXEJGM9lcu2irlD +mikPN/EyqgHBHIV8LVuOOTzZkl1n2mgiCunxg8cb +/hJiyMWuMlsg3TAch9KPsABIaLtcQxWhjiUS79YB +iI7belh9jCK4ARKWim0pJgqSc9xWrExqhFArQ59I +PRdMjcmRD9Q49yyuS37ztnHc/iha/0gAuAZT52zM +DXDMGy2/L1fuOwkd46W+iLIPRn7J4YIr8a6G8ce4 +tOoC9/k8KplbVRnjZL+7ZDJeuuo0MFt+LckmTFQe +S9Iwueg44XLG/U27UYBM0LKK3UL4ohxLAtgJq6cd +ZEZmcaHgBrCT25o9pWp4nRwhzkTz0ZOrASFiIRjh +2YnqUXJn/pyaUPH002Vx27TKCDt+0ATce+VGuo/A +Yc6qw0z0BDmG6kEu0JQRwxpGl42hky4cQa0NU9gR ++4jmQWip0GOC6vpSnrW0UQb7pXVyNRd/pw4BlfZu +aUb+gpe5Smt6day/6U/4eIWiWz/3PFh5KhcnRUb0 +3PQUIsGT/HG/ZOrEpIfDuTGQNyTRRKuxyAtr+Mxh +nlXp+Hr6PwdrGyHLHK4WJwpdzJ/OOBIcBvneF0il +qO3PHY11wPUkrYyJb4FbTKPY1KcqdaK9dwGYFWU/ +IPOE3ovflMpqr0tQTLgKaJIHG/qhBdnIVb1xFY82 +Th2pd04aQnULL1aLFCOHCFRquTuk6MmJkNf4vfAb +bnsvbnhhM1ENy4PjRHJoobcs4QNcZtRgKWPPt175 +KbOuYYRcXT4clB04dr/SQzHW54X75e0OMsFRJoo/ +XYlPwDiok2FfmAQkbOL9kGYErP9ozoNHG/C2hV5L +1Pyi2a2looj4xCIkTYGERdHamb3INeu6rCK9TMOw +fZqdKp1N9FANzGCwFgZpBys2zflSOJZTfYXa7Vo0 +kdBS4hDhqzJUDFblP7VrprVAHEsUFzh7tdAPQgRt +OSY5bHmhbyJGGS4euDI7l/2AB6CFXjXAZSbF6tc9 +DquAYzOduLT5iVUAMBF9gFAaEvE4ON29wR/kAKfJ +f0RdXMWfn2z4ip5/LKoCt+dGo2o5YdCK0lS34CRH +Le/5knAWg9N7zGssQB3iYCj60dBixLrs3Z2/qv8u +95tNqcD0do2RaJa8Nz325fLS1700Cj7gnLbQl2fz +SSz0mnrT70ofXlU338KdXWvJBf/GI4SsiBZXclDO +0nfO8FWJR75cWLK4hMIAx5R32Rpg9Wt+Mhlug1ag +oJ8OCe6cb4MbdppbegBvE2d7bF6VLtkRWqlrIi0Y +l6siQln8NpTPKl3wsh4Pc26bzxBCZoEyvPaVeNv4 +03J4+LEhGtWWEY1JI7g6DZWwH2QCbl/9P5TlgG/n +fm96PJBAzYh5UNa04tX/t57NQmx7VZ+jzWnmPKkN +HuK6QQTfcoLMu1mUEgs00DBrfmPv4L8TfClToUwM +WZuUQx7hImJKOoYek4OQzp6lT0BXYSLcEwj9Nw4l +GHlGdQCuWR86XVSRHIdI/aqSawX3VkrFm8Hlv9Jx +ocP8E8mSlGsMe86VR6DJVXrOVAmucjEgEpFUEi0d +dZ/00N23vpF7XeuxL2hcnNFHpMkAWze1g86F9jD1 +YdI8ZII8KessxkTNlFNctVfpcEjrasfBAX20uwkB +/PxmDgLLIXfs0N1yttvGSF5dJ9pZrKOB5ZVeR9g7 +FS8QJA5mFNa16V8UYYT1bxuqR4IbzYN9pUblw3bN +SVy5yj1jFG5yaI9G+qFxuxC5svoMnXvSUEpCxS4o +9yIfka1ur4g9MZD1B60BiO2RkH7DaglOLuAKn+SA +P3UTCeOr4qux9j8LkDhq5AsCeR6Ubw4ZvtYpOyMy +dXLcsVim1T6AtVbB19U++04wFWEu80/54J+og4t0 +0DMD2++wFrziN3YmnaD39OiGerUM+jwvcvHZmCQ9 +S/eEnygr72pfkEORoCPZTNlKf3NwvPSy+cdN7l7Y +kY0bLtX2/rxHetYrxftxWQ0ssklyVkuo+3wqUpte +rawVUWVUzxiO329pwd5B6G9BA3WIKGdf2I7S7vnR +yJ/S2bNT3x5QwQW5tJlHG8DVmMhM8plpZUZAKC+d +0UvhWDNdw+OlLldNr93KDSLWKHXTNarnk/SSsCNx +22OiyLhHueCXa1SqJY3JU6dugNpHnsuZeGBOHHAB +NQ7caqEy9+LtGXwhA7b1HVcbEMd2Rr2h0GQVNsm0 +sHUX4foD0iSMj0k6agGbwS7aUbEXwIj6kLf8/QBL +B83gKE3lOdw6C9jykK9eYrSUd8mX7sCjWJ6Yo99v +86oRV1msBS+3z3wNhaH7xOuwyeDQnsJlvWGRIkUm +WqvLjW8+zl5r0taWjNqYUoMVmjvR5fIvlCJkuZgC +MWl+BUk2LAsmIRWFn4xoF6VDHurlCX2frq7zRMVo +Q886GzLggSRYC3BFs9L8CT5XmAnyOYu2JVC7mJ3f +hfsA/L3eGpOwgq8VHLUM5twnPzQUk3x7af8cnaez +em0LxxGgPkLbX5837HkmqmyPJQujy8sdriaPGqad +Z1uiCUKpsAMzIwxXNutmeJI9ieKLYwpqyfgbhxyq +fi+ehbFCU6jWDNsF3E7E38gW8Hyiz8yWJ1zmVvVO +FwNS2XeJwNUc3KLAbl5BtNkMXy9/j280pevjYqKL +YdyrNlKM+YyXxSH8YKM9QLDw0b5cJEcf9+aMgXtW +h/Q9CNDkXAjRJqXShKsZgwiY3rIJ0cDPFyOjbygR +iMZi63uMcz6nMAQtJPsVZUSC1iISGECBxSBklK4r +HTeD2hfd8+dI5NyQBhLLIFza+tfEWDerdMuL0d1P ++xy8nQJlvgep37go7B5hWl2F1+CJFGp8A0+HQ0F0 +J2orYrSjAmWqy7NREg8lIG0z+OKle1yi3Jmo6n0B +bhpk/gmxPVJdpV6CvLKWXaf/4teUwhKK5Arbf1I1 +8M0XEvkCTyVUnmnhttUAUO8JcGmLljziVLKpTf/L +Pj1dBt6nfmN8BQgy2ZbDJU/0p0mslodNDfj6ubth +NrpPbHEITLWfKf1/g8zATU85KJxvRMh5wMX2/lk1 +ihB2b0RBtsZwnau5gk3kYDXXZPSc3aELrL9NeVnb +0lEKHlCisCgwhaUjpsFDHQpURDSy4pAQK0w6lzOt +q1rjmFw9EBSxOgsl4HdLW3dQOZZ6gnJxaj/sdrvQ +mh0+5x8C4pWyxiBM1Gurwk0OTwi2hd5CcWZhqVyU +gZvYwdkXrT6n9MwjRrZE6a/DMFy6CPK04vUWvt/J +qcBwuJ2x4QPnuKdfPTbCmVVgyiVJUXrxpmrtZhkj +elDypRkyvLIg1xKbDEeRS2/rO5AnczFj7jwaGLZV +Alf7Hyg9BdGOtsepK6wV+d59CPWD4yzlDcdUSbeN +cTVyac4Pyx2Ez+gwNdt84gKSzMBYwHa2IZWcmed/ +/CAwhAgD3CX0vMFSfZfNhRjFQFgzak2KdMWVolMQ +Uvii9TEZBqhMi1fx94zAOTavImZy7S3WA3Jfyxmb +2vHAKYp8VyMYcGS9k/3q3hU1btbLCXao4scmE2hs +X5bGpzGGkDOtiJzvMl8tWWtUa2Af070K8qQPhY6x +Aqu8B+2aVS1py73v4HoqBSf4gA6BiULptZcqM1tt +1uwOmDKZYEVrTnLFwaadonLGBvQKRNzw/8DVbUj2 +K6ofXWGvI+DK4sqH/yiv695VDuZ0pLeuN+jBcMqk +5z2fyln+IbE0PGGKU+uTcRxNPz6Zf7pud0iKrPL9 +qwDT3yLh9GbAlpnjDWJ7mEHTacmfADM5bEtxKI+F +xvFRd+hjKIvZQqiBwOnFaa2sDmXVaaY4arinfm1Y +98BrlznGwlcBo+/O5tWTZRyK+aZdRuIcMOyjLDNV +8nFbN8zj7TPk2MZVHBH2YNZTNSU7OkRPf6wGWApk +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 +isxgGVZ1wJSbUvndgTd2jzFWeL5rhgbE2VKdpDfV +P+uvrJ1VwlO5FRbKtbvxsXpzDElrFguhoHnbj7zd +3k5XtsQFIWiXoEPbe9RgigW4hF0fRYZMwQCYLKjd +4RBNnrPh3zIW9kpRabfN20gSIuiqInIP8dKUKiKY +0opVHhKUB49IAey7JxqBnu7hXOu5AHJm+KodDarE +dmOY/jZVRExl9HQF9A/zvjaNYpR82qloKkmoqxQi +ffw4I8lsLw3E/gOMbeDvNWvQA66sSGTWIZaQYqmZ +gkA4oru7nvpDdLmiAxTyer0PAcvEDRgUvZiM0yGG +keed8RRIJ6vlCnevxo4z3BLYcGEasVazpxfDEheM +Sm00WPpARYSU0UGkzq+DyO2u6umkTs+FOlSnxi+Q +2jWIJiiZLNTe93GpcRidX30rCWWs1P052erWehgV +HBY/jnJaRcSZ2YCVJbR7YEmbUuyaax4Q4tF4DYaE +tW3ogBfCg3vqCdh6hAzn1JQPThf4XUjuiN3zSzEn +mnA7uiE+ddASAx3EgjrxRrtvbFNQDxSH2AqIMGLP +1/vEqdNX9LdgXk8BeMgbxBOwXT3tTf2vg4GnwuRD +DUxL02WHNbDtAC6BLE12XjW+7uHZRbH+UG4JjM0o +tgG6B2941yw4+2zffKB6Aj77djFIN7cktP4+A6PD +jNKMu+6JF5/zo453NbKvx/ITlguiqI/+J97AD8mA +0fg7+6ZGk/B2gZmq3O/LZ5zYQyzevZnZpA050IrG +6hTK/lcO88CwPeg6aX1MmzasIdDmdFda38rHlK+N +oojLvEr/H/KiPYklP7cprAzksYbHkAS3VknoY494 +CljIfNqDrTUBaFCJXn8hIQCa0GHfHa2TyeBkYx6K +TsWjLofnN7IB+aDg+/RHDAi/JgotHU032HH/tK5m +TKIyvm4adZ7eKDgoSsiVBaKhgCy0pWpw8BFXEaHF +bI9h6E7kPr+T+2S9LnufEkMBwZnNdgVdU8Ue3Oy2 +BQ5Vka/ZRCoU7wHHH0Pf7+YJ923SC83GF+3wQser +J6e5+j0ucHps4O1XQpERifrl2VDqipEG/m1FZlZD +YOJwqKtWP9UC3L+yK/Jq30eFv4JnbTA7zfJYgLAB +PYscJVNjqP4n34LxTrmSoPYrrANsWItq74LVC2AB +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 +B3BZMbiDXGAG9mHYbLCKy7/BUrzIIkY/ooPWxb8H +MXPBcuRABsbTCHka8swYx9QMa59lvdi6S+H7N+dS +Ix1TmQ9SE2WjhuPsuydtSdIvqLc2UOcsEkGVvFqu +fF2Gmum975aoeNjw5wSEyXnb01yEZ2pr8Eoq+yE4 +Xh1B3mWkWhQDH1bx8EfhFaL3I+fhZklj5Rm1uIFq +oAhzPl5xilJLYuBiuqcyBxj/TY+iTgSql3tpfGJ7 +YUCoEI+ErnK9JdxOJdY3Kp4JEfnI52Ia7xDOvzVi +or3lm5MJx8OBElhsnVmcrI3Ev0YnrI0KPLw00MOF +pfscjqmIFCoCkrI16+LYhYO9UodbNwaC7MsmZUFg +NrOt2vSDophRHRXGmqypDFJ8JVwgiq0ZM9R+vFtF +kJWJ37YQKTEaNn3WH92vMwIlKTe09N1PSEd86sR9 +BdBCzNc0rXSpLOvzDVvfc/GeLuNoTU6buQm3D1VH +f0UXpoG8eny97lryIrFKiwNtReYkwbdxMEPB9lCs +199YelCmaT9mJpfJ5aUXTr8f8lSJRNvSAp2a8DzF +0XB8KiX5JjbZd+scu4AftTqHOIu2IX/PVeReQYnF +JV5ZTeGzFhTqKj/8HbyOwNVtwZuQYS617Zqh43SI +ismAzX25xzM91aICts4kb8BbFd9UKm7Ct/pgyLUz +N2vJbk0jbG6e1bG0iK2MmClbUnzPge8CvnUJUdew +h3SwK/Z4Ml92xaxTdE2/iuaJoX0re+nA2jOcGITl +JxfVzVuZH31tpZFJRy2Hu66gCzdDwTTXJIGJf1Bu +PKrBGYM0efnOQ2lNBmDGzNBGBF7AVuE74o65hI03 +9EAMbiyael20nvZckcGgL+cvyjUmlZe916jVo6ia +aLaRnlkNpRZhyWUh6FthtdBJ3DOV2/E8KOjP08jR +l/jrcykJx9czqCecgBXfad+wRgCZs1DuVg7f+DVq +aZ1M6lOhqLq9BhLoiUXQHKqP6AcX28wkJ30FLu5E +PNoz/wogAjjLj9zS/lt5OxQiY3P2Q+RVFL5Jov9f +sHkDXt4H6WaVqNDL0b4Fega66qIAFGj6snhnuNGJ +4NzjwCFOrL7zELDqqQ5PaL+7F9IG6kGHLINQ+oht +51gfef52Jacvnq50e5LUCFR6yWfzUl9No8ocaPC7 +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 +/TVSElr0yZII+P2qCZSrsMWCklu5LQcj/skfuO8E +U9nCOgkqnw/kdNWck4R8M7KfkPyroH67CAmcAK3C +aEhTq17a2sDMKwhY2lD5uoLNgUykgdyJpqWmmNQ5 ++ZDNyCmzLHpQ2CHkSi3IpSpsjoUrGOZHh51/m2Wr +VDyc8To5n2hUMY0D+3IpjGmfhnNRQbH/C9Qvj7dZ +L9NGNa1CNxD487kBaBvu+LlNRHxBpdovOqkPbIy6 +UnctZS8cCMWW5FqTfa4fLfP7CW/MsR784Bs4r22i +ZOe7ms9/Z3MCxMggMr1GKTeUH2aBQ7GpYkI0CdWu +ynqPKOYSBkUpob1J52+hCqBTSJpINyt5v3cnDdb8 +X17h4+P2rqryI/y33MBTf+nVC1ERA6kDxY/6S7f+ ++xnXGtFYNxKRVteWHj0OTN3K1fylKTf75EQCOsL+ +DjOiHjIlUWOHBiv/Ha6x3IZVNwCu86ycs8F/Ov47 +cX3523A5X3QnkO0+FhiMNaZf2Cr2L7iPWSvKyrgZ +T3gsO7+6w+TxE4zKnrIcF5/tAcsU1S/zaX6JsZnC +9/QonFlLFnXrDUiSRGh7QElHHyad6jKfA6wsFMJl +nEJ1LJoVkXR/4tMWq6sRQifLTiag38Lx6h/hYwrF +4uW4+Sh/vjl6aw3pkyhdZFz9pMGSb6l8qDGqcgum +xvGrX1p2UjjkIWgS0VJlvgy1uNZW+CnW1faYfMwX +ZTT+pQS+GrhqgV0f8A3eF2T6WRTJ+jwGep2+hg98 +fSJhTWljHZtoCzwJWwhamodxLbgaT7+JY2L+BYhP +v7I4sV5wON/KdClHfG7vgiiHrt+CmeOXqdmF52A9 +nGd1fanlKzHwAH6msAFlOpwMsrK/EDNWTKNQK/vY +sHoPLWQSJNPe+SCbA99a+ehow42dPgOdtywIiNx5 +AzbZge8N56/zBXhHPYNjTCP9Z9XzuYggO022hl+F +7AoNtBrUeaRU2meX6Zz7XbTlnv0j4Kgo5eOdwLGD +S75idkMSTlOQU/BEt/RQ9eBoEadRAmsFxTlfYs25 +T0QhrupLLHQlWnWyoDlEet2+U+RvqY+xnuDew0st +Yu+eJa15bhO6srSBUUzuz/OUK1/68pzlqX6cc5Ec +qwmAUj1BtUmjetyhTQN/gHu8X37cYV/VRVzV1Rt/ +1kKqNF+rdVV5wdgD8jIGjwmE84tY7Xwoj13qvirz +Ia52v2wWTu/ZHagTGT8aF4ZsGqPQlYM3yoxmvBkG +tNygtIoV2trEKqW173xsrOqT9rPykgI98JTElHG/ +4MK8VmdKIDJKcNEuzdZQoFCudWX/ji3qswFzn+SR +g4veUpB6Z6H71jveDl3POR+eiPHmi03Clhyndga8 +pKiW905BzixWw3o522hFLTtWSctbdBS7HlgcxxwT +v7TfQ0j4Ke12cuHzeyxMY7Q6AYofUx615LEngYGh +G2//4703hlJDcs4jeQ1Kn0w0cu4mc4TatrubCY6v +3sMv+0IMDu8xxdmilZKSCECrffdlpGY31OtSyDy9 +v8KvDM2NzRwL3bbL38rL1WjyusjZSiY3Y61hN3N6 +YhlHN9wQaYmtZV2b6kWoI5bJeySmDUaaTioFs6kT +/XbDF2E17om6XMMiBRUZFJHUGHp7HjKgKs+WM8fu +iy/zRmNTtiTFO+paKNtXCshPq+jIh9cvWdeZl6vD +/3kNbAu5wjL9jUnfDPRR02DtpnQL2Q+4I79gt6FI +JB0Xjd2uRexIdpSf0WZMEk2bF+fn9BmD7WVE7DG1 +SMJMoP2tuz8Vwv0J4T3wYu25u1R7/tZxrG5Ljb0S +8b/8STrBQLDvSsG8vfqwVJdSwh/aDkOm9LmHZ9m+ +0vUKT7YCqFdOcRcSB6ycuLyhSUA0IGB/QkYdUO2P +hHsT7S/51//Bp+52konYVtSyO8qVda+wKlOEhLrZ +1fH51WbKCaCuerHHuZQJ9oCWXK0u+BBQyYXln3WD +1dtFxw4zJJ97GweQc1PnBqpvZ32aWnSIccHirdqv +/wbp1uTyjvx8sQ4JP4rFvf822MhFnJSWUfYugDh9 +3XEJ4zm3Ku54nYNeNgT1kNKsMhZapHS/r3oIQ17M +naJAjOKslBLSvWr5sHYuYkL5Q5CyJQekpyH1kI4O +Rw3Tl0roycI6MIflG97Qbg3p5dJMF/ZxeBE6LccU +JEJIhiLYH9FQqjCWR/CuCpnIBMNymMWtmX3os8nt +R364p3FFsqZZ325+lFVgpkW05ceyBYUDyYNr+QRs +9+6P17z2gD/PKsKq2+BR4k+two+y4qspV+y+qxpR +4x5kIKRkATAuyb9SCx7r5xnVvT+/pLdVyzY5RvEG +HFacHrP3gg1vPgYN0dhMWhb43JsYa0+68nVEWMad +ULDsMyC+1Rg/y15x/8oATArtRj/58Cf2o6eD+l9B +/RcSgZ/NndMBbTuz+ywSqSWngT9SIRJLw0MFgwN0 +UmsuPq4S5EqdsfVow4M0UPI9CkX2/+bH5OwLmNDc +3u6W4AShO4YraShWTqOn4pRK36ddvv8LOJZN5BEg +mASGVpnU8+5mWJF85ER9aYAeK1PJQdR6gnLVdnjJ +49SIFaDcYIo+I10v9Cm/4m0v21/Q1E1m51C75aXN +5dU8/oLTYSmWKz/T8BRdP5IKrIB/eqFxJ/BzFQfV +oqWN32jfjpuCYiGuLU/VN7I7u2/bzcyDufsJmZQI +qvSm69oqUblb73JYkQgIIga1Jag3abUeqTh0/SBD +z5HI5fceGwuRsFl4kC47wUHX1a63NRQ90uEdQbfq +x6ARVZ6y31PMSPteVd6fPj/ujzr9d2/flC2nMlcZ +MRnyFcWPISXWVNWDdvK5QJGt9EIrF69u2bm9/D+O +H/9cmJ3ONsvJethcFPjsnqoPUVJ1siMIEKI2IVsl +1ObtjrbjhIbfaXKG8X/s7yX+7Yrd4HjmYZi8Dc00 +a7L48r7f6mLvUKMkhZiPZwNw7irBtV2OyEytTc95 +2JL2Fx2rbcGCvok/NDaiIl/nmWIpC72KGbo6gDH9 +o7qJPDG5+Gxb7bVVaeWZh0va9CTTjnWFAWcqp5ZV +ExvbiryfgUOxhDYoF+k4eVN37dzgUwc3uwx+Kggj +FHCCsniatMvM8vZBDAk+jwccdvFiaq7LnaEogqin +lLZRSpyY1CDXH7FflL8r7SgBPCAa/3FTzW5//mNZ +xUsp5weeYnVnED3oLt4sizWFv2QYVg23egs9IuRw +2+RXQ2+imDQRlMT/glnMQrga+AC9/fyYoMWg4VJc +tCSaREunIyYpzfdH8Q2Oe+LfQwMIbSL1x99W8GID +oljCzJPOFeh+BMogF1DCFdhnO0XR3tIiVQ8YQKHJ +JvzK22NIKH1iK4qmhABR4ilC0v/Yf90YTrodHs1V +MN4YRciKcNnohbVPvgbrbS5SyIVTSHWyRwB8rLc2 +XEisdN7sfw4zjM0VdwXVyNMCd+Kqf2isazY9UepB +Yksm57f8IVLTaLyeb64hP+skLKu5tN4K/kcqG+BL +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 +J/39LmTEx6HGpeCg2zxMiCcN/1doEgHbLNzt0Jav +hxTavoNX68b8cGAN4OC8qg/MQOQylcPsYpV76QX3 +svwlhv2y2Gr3P21tMv8B3GambwRyyBo42R4/IxXV +3QmiKM8pQpKEkL9KitmX/Dji611Ne1+iuufoY80a +9nQrqKaaY7mdA8cbsz1aFfUYaJTE8nX+R9nEq5ye +7fDr6oRr489Yt6FxmEOkhMaBplsRYJ0yaLNEqV/w +5Bx1vh1barTdw9C2kYV//gkrfu3FlhEI+doy8gpS +VzJwA8+5i6SL5MQsPGlQeYuwqmr7Aw8enNoG+U3M +6yZ1dHWaU78KglZxGrCgCICGmGydrRNXdoXcn9mP +egvJSDEhb7/NtUqHuIb6lYDQfX5uQETNGmqobYH7 +vvxw0rozp6RqEuGlSi0W80oNMPHgRMXGfLekOLeT +6MP2mgNDLNY+QwNh+tX2IlfkQsRhUgvZNY19P5g+ +ID/Z/eOJIxI5nrP5mqDZb+YaGv2ah7oOiwPRvsYj +If24aiPaYdxBWDwv05HNYUTixRXpKyz+PGXA3ZzE +KftDzFvKPgNwnZWDxlgT2eRKeD3H9Dy7M+XEit3J +ZsyZ44ISCrQ36dHNbkXzjpIUgCcRpwlmj/d1gPAS +utabqop7qXSftSiIC+nx9Z6ep7bBvQBYc+RWeB/c +VAk4bhHHdEAozEdxVyiSG3+nXnqOCF97ZRqBiUI4 +W0U1My9XXauT40B+ZXamkaHlmmHsFceKrq2ne8EX +lPqQ8XOjuUTbwI4TY2UuBUX4GT2hY4RLhzShtMcN +h7cerfA0HlwL1e7yimc+zx5otHqEv2knYDfHFoNt +NFsDwfuc0tsWPDsO1GEpStYGvb+/YZkazAdmOtSm +VRPT+0Xquke5soNZ2qDNYWewAGAJvHm/gaBdqLFX +dGJyrIZRqBZRwXyGEzElNF9VoQovADfwNdiuXT0q +KoT5Z/YxvK9mHivwG/ZJ/xKGAH+5648wLwMbDlJX +zErwvyJN3Md3ax7BiNeAQa+jd69vY0VzmK/S3uwo +HpymWQw53HGReYANozpbTHYTBlCwqFwx4PKbqBmf +SRNA8pbzCxm8kodPYMlS9Yn7cV3ChKO9QOSg52or +pCsRr64ELNTNh77XOQFmwE3aQoH6zgBGsleEE4iC +IEQXLSrCyEaf0M4qFnHj5pCHg/OSZF52VrS4y9h9 +OZQmOxVLtHr8C7jgV1ij2SHuKnALXq+TsEL6zHZg +r8Cs4DWxWfdOkA2HE9EqArybL5uYAKF9W58LNhb6 +mzmhBSwXM4lI9N9n3XIwEk0xm6Mui3veKCW/owl5 +WTtr3ot90osTdLFckf45c5A25S79WYk9PTTW2vT+ +d5ybsVMQWvGEF9VEEeKIIdIp54djsFLR1/U7av2o +Qk9RM+cWBU5BRdEpP7oTOvi2tUAg3Zf/9wZFuPwF +tcQMmJrnVQvhWXR6aakDacBE/Js3qu9dh1CvISas +7pl3VRQA85W+G7GbaAHhwXHgR7riqaTyMxDLVIKH +PGcelc+yQ3zK6kscQE9dATS4CzhZpy0Q6YUjh1FD +1DOpbCYsr/S6F/3Kpcr33iF/pn7wG302twG8j7wN +XhAqeDp+VQTST45pJcn0/s3d/LcqWWj46AMhVKIC +T8r5uN5rTDGViZLtMqOX3YLjLgLKwtP87yMJ8MA1 +D0fOSlzUaa4Cnd4HTXGGPORmXce1bWmlbjMpIbuZ +6rIpNdnZbQAQ6lKJcDuqZpN0lA/Uel3GtgWDqjgM +7qMcslIjVPj0UJ/2ibDreNB5GWM= +-----END REMAILER MESSAGE----- \ No newline at end of file diff --git a/mixConfig.py b/mixConfig.py new file mode 100644 index 0000000..22de369 --- /dev/null +++ b/mixConfig.py @@ -0,0 +1,121 @@ +#!/usr/bin/python + +import logging.config + +_mixConfig = None +def getRemailerConfig(param=None): + global _mixConfig + if not _mixConfig: + _mixConfig = MixConfig() + + if isinstance(param, dict): + temp = _mixConfig.copy() + temp.update(param) + return temp + elif isinstance(param, str): + return _mixConfig[param] + else: + return _mixConfig + +class MixConfig(dict): + def __init__(self): + #Matched with startswith + self['ForbiddenHeaders'] = ['Subject',#This one is handled seperately + 'From', + 'Sender', + 'X-Sender', + 'Resent-', + 'Approved', + 'Errors-To', + 'Message-ID', + 'Path', + 'Received' + ] + #Matched with startswith + self['ForbiddenHeaderValues'] = ['Authenticated sender is'] + + self['remailerversionname'] = "mixmaster-faster" + self['remailerversionnum'] = "0.4.0" + self['remailerversion'] = self['remailerversionname'] + self['remailerversionnum'] + + self['remailerkeypassword'] = 'FILL_IN_REMAILER_PASSPHRASE' + + self['remailershortname'] = 'FILL_IN_REMAILER_SHORTNAME' + self['remailerlongname'] = 'FILL_IN_REMAILER_LONGNAME' + self['remaileraddress'] = 'FILL_IN_REMAILER_ADDRESS' + self['remailernobodyaddress'] = 'FILL_IN_REMAILER_NOBODY_ADDRESS' + self['remaileradminaddress'] = 'FILL_IN_REMAILER_ADMIN_ADDRESS' + self['remailerabuseaddress'] = 'FILL_IN_REMAILER_ABUSE_ADDRESS' + + self['adminpublickey'] = '' + self['blockedaddresses'] = [] + + self['statsdirectory'] = 'FILL_IN_STATS_LOCATION' + + self['filelocations'] = {} + self['filelocations']['adminpublickey'] = 'app/data/adminpublickey.asc' + self['filelocations']['blockedaddresses'] = 'app/data/blockedaddresses.txt' + self['filelocations']['iddatabase'] = 'app/data/iddatabase.dat' + self['filelocations']['mlist'] = 'app/data/mlist.txt' + self['filelocations']['rlist'] = 'app/data/rlist.txt' + self['filelocations']['pubring.mix'] = 'app/data/pubring.mix' + self['filelocations']['pgp-all.asc'] = 'app/data/pgp-all.asc' + self['filelocations']['secring.mix'] = 'app/data/secring.mix' + + # ====================================================================== + + try: + f = open(self['filelocations']['adminpublickey'], 'r') + except IOError: + f = open('../../' + self['filelocations']['adminpublickey'], 'r') + adminpublickeylines = f.readlines() + f.close() + self['adminpublickey'] = "".join(adminpublickeylines) + + try: + f = open(self['filelocations']['blockedaddresses'], 'r') + except IOError: + f = open('../../' + self['filelocations']['blockedaddresses'], 'r') + for l in f: + self['blockedaddresses'].append(l.lower().strip()) + f.close() + + def allowHeader(self, h, v): + for f in self['ForbiddenHeaders']: + if h.startswith(f): + return False + for f in self['ForbiddenHeaderValues']: + if v.startswith(f): + return False + return True + def getMixKeyHeader(self, keyid): + str = self['remailershortname'] + " " + self['remaileraddress'] + \ + " " + keyid + " 2:" + self['remailerversion'] + " " + return str + def getCapString(self): + str = "$remailer{\"" + self['remailershortname'] + "\"} " + \ + "= \"<" + self['remaileraddress'] + "> mix remix reord klen0\";"; + return str + def getConfResponse(self, keyStore): + str = "" + str += "Remailer-Type: " + self['remailerversion'] + "\n" + str += "Supported formats:\n" + str += " Mixmaster\n" + str += "Pool size: 0\n" + str += "Maximum message size: 0\n" + str += "The following header lines will be filtered:\n" + for f in self['ForbiddenHeaders']: + if f != 'Subject': + str += " /^" + f + ":/\n" + str += "\n" + self.getCapString() + "\n\n" + str += "SUPPORTED MIXMASTER (TYPE II) REMAILERS\n" + + pubKeys = keyStore.listPublicKeys() + pubKeys.sort() + for k in pubKeys: + str += k.getMixKeyHeader() + "\n" + return str + +if __name__ == "__main__": + logging.config.fileConfig("../../config/test_logging.conf") + dir(getRemailerConfig()) diff --git a/mixHeader.py b/mixHeader.py new file mode 100644 index 0000000..51166af --- /dev/null +++ b/mixHeader.py @@ -0,0 +1,145 @@ +#!/usr/bin/python + +import struct +import hashlib +from Crypto.Cipher import DES3, PKCS1_v1_5 + +from mixMath import * +from mixKeystore import * +from mixPacketType import * + +class IntermediateMixHeader: + def __init__(self, data, decryptionkey=None, decryptioniv=None): + data = data[:512] + + des = DES3.new(decryptionkey, DES3.MODE_CBC, IV=decryptioniv) + self.DecryptedData = des.decrypt(data) + + self.EncryptedToPublicKey = hexpad(bigEndian(binaryToByteArray(self.DecryptedData[0:16])), 32) + + def pprint(self): + if getKeyStore().getPublicKey(self.EncryptedToPublicKey): + print "\tIntermediate Header Packet" + print "\t Encrypted To Public Key Id:", self.EncryptedToPublicKey + else: + print "\tIntermediate Header Packet (Encrypted To Unknown Key Id)" + +class EncryptedMixHeader: + PacketId = 0 + TDesKey = 0 #Used to Encrypted following Header Sections & Packet Body + PacketTypeId = 0 + PacketInfo = 0 + Timestamp = 0 + Digest = 0 + def __init__(self, decryptedbinarydata, ignoreDigestErrors=False): + self.PacketId = bigEndian(binaryToByteArray(decryptedbinarydata[0:16])) + self.TDesKey = decryptedbinarydata[16:40] + self.PacketTypeId = binaryToByteArray(decryptedbinarydata[40])[0] + + self.byteIndex = 41 + if self.PacketTypeId == MixPacketType.IntermediateHop: + self.IVs = [] + for i in range(19): + self.IVs.append(decryptedbinarydata[self.byteIndex : self.byteIndex+8]) + self.byteIndex += 8 + + self.RemailerAddress = decryptedbinarydata[self.byteIndex : self.byteIndex + 80].strip(chr(0)).strip() + self.byteIndex += 80 + + elif self.PacketTypeId == MixPacketType.FinalHop: + self.MessageId = bigEndian(binaryToByteArray(decryptedbinarydata[self.byteIndex : self.byteIndex + 16])) + self.byteIndex += 16 + + self.IV = decryptedbinarydata[self.byteIndex : self.byteIndex + 8] + self.byteIndex += 8 + + elif self.PacketTypeId == MixPacketType.FinalHopPartialMessage: + logging.warn("Entered PacketType.FinalHopPartialMessage - UNTESTED CODE") + + self.ChunkNumber = binaryToByteArray(decryptedbinarydata[self.byteIndex : self.byteIndex + 1]) + self.byteIndex += 1 + + self.NumberOfChunks = binaryToByteArray(decryptedbinarydata[self.byteIndex : self.byteIndex + 1]) + self.byteIndex += 1 + + self.MessageId = bigEndian(binaryToByteArray(decryptedbinarydata[self.byteIndex : self.byteIndex + 16])) + self.byteIndex += 16 + + self.IV = decryptedbinarydata[self.byteIndex : self.byteIndex + 8] + self.byteIndex += 8 + else: + raise Exception("Recieved unknown Packet Type Identifier:", self.PacketTypeId) + + self.Timestamp = mixTimestampFromBinary(decryptedbinarydata[self.byteIndex : self.byteIndex + 7]) + self.byteIndex += 7 + + self.Digest = decryptedbinarydata[self.byteIndex : self.byteIndex + 16] + + sanitycheck = hashlib.md5(decryptedbinarydata[0:self.byteIndex]).digest() + if not ignoreDigestErrors and sanitycheck != self.Digest: + raise Exception("Did not hash encrypted mix header to its corresponding digest") + def pprint(self): + print "\t PacketId: ", self.PacketId + print "\t PacketType:", MixPacketType.toPretty(self.PacketTypeId), "(" + str(self.PacketTypeId) + ")" + print "\t TDes Key: ", hexpad(bigEndian(binaryToByteArray(self.TDesKey)), 24) + print "\t Timestamp: ", self.Timestamp + if self.PacketTypeId == MixPacketType.IntermediateHop: + print "\t Remailer Address:", self.RemailerAddress + elif self.PacketTypeId == MixPacketType.FinalHop: + print "\t MessageId: ", self.MessageId + print "\t IV: ", hexpad(bigEndian(binaryToByteArray(self.IV)), 8) + elif self.PacketTypeId == MixPacketType.FinalHopPartialMessage: + print "\t Never seen a FinalHopPartialMessage before..." + def getPayloadIV(self): + if self.PacketTypeId == MixPacketType.IntermediateHop: + return self.IVs[18] + elif self.PacketTypeId == MixPacketType.FinalHop: + return self.IV + elif self.PacketTypeId == MixPacketType.FinalHopPartialMessage: + raise Exception("Never seen a FinalHopPartialMessage before...") + +class ParsedMixHeader: + PublicKeyId = 0 + DataLength = 0 + TDESKey = 0 #Used only to decrypt the Encrypted Header Part, not used elsewhere + IV = 0 #Used only to decrypt the Encrypted Header Part, not used elsewhere + EncHeader = 0 + Padding = 0 + EncHeader_Decrypted = 0 + DecryptedHeader = "" + def __init__(self, data, ignoreDigestErrors=False): + data = data[:512] + + self.PublicKeyId = hexpad(bigEndian(binaryToByteArray(data[0:16])), 32) + self.DataLength = struct.unpack('B', data[16])[0] + if self.DataLength != 128: + raise Exception("Got an unexpected Data Length from the MixHeader:", self.DataLength) + self.TDESKey = data[17:145] + self.IV = data[145:153] + self.EncHeader = data[153:481] + self.Padding = data[481:512] + + self.TDESKey_Decrypted = 0 + + ks = getKeyStore() + privKey = ks.getPrivateKey(self.PublicKeyId) + if not privKey: + raise Exception("Could not decrypt MixHeader, Private Key for " + self.PublicKeyId + " not found in keystore: " + str(ks.listPrivateKeys())) + + rsa = PKCS1_v1_5.new(privKey.getPCPrivateKey()) + self.TDESKey_Decrypted = rsa.decrypt(self.TDESKey, "This is most certainly not the key") + + if self.TDESKey_Decrypted == "This is most certainly not the key": + raise Exception("Could not decrypt MixHeader Encrypted Header") + + des = DES3.new(self.TDESKey_Decrypted, DES3.MODE_CBC, IV=self.IV) + self.EncHeader_Decrypted = des.decrypt(self.EncHeader) + + self.DecryptedHeader = EncryptedMixHeader(self.EncHeader_Decrypted, ignoreDigestErrors) + + def pprint(self): + if self.DecryptedHeader: + print "\tPacket Header ---------------------------" + print "\t Public Key Id:", self.PublicKeyId + self.DecryptedHeader.pprint() + \ No newline at end of file diff --git a/mixKey.py b/mixKey.py new file mode 100644 index 0000000..72fd548 --- /dev/null +++ b/mixKey.py @@ -0,0 +1,441 @@ +#!/usr/bin/python + +import sys, struct, time +import hashlib +from Crypto.Cipher import DES3 +from Crypto.PublicKey import RSA +from base64 import * + +import logging, logging.config +from mixMath import * + +def parseMixKey(lines, passphrase=""): + class State: + CapLine = 1 + Header = 2 + Body = 3 + if isinstance(lines, str) or isinstance(lines, unicode): + lines = lines.split("\n") + + state = State.CapLine + for l in lines: + l = l.strip() + + if not l: + continue + elif l == "-----Begin Mix Key-----": + if state == State.CapLine or state == State.Header: + state = State.Body + else: + raise Exception("Got the key header when I wasn't expecting it") + elif "Created" in l: + if state == State.Body: + key = PrivateMixKey(lines, passphrase) + break + else: + raise Exception("Got the key body when I wasn't expecting it") + elif len(l) == 32: #Assume PubKey + if state == State.Body: + key = PublicMixKey(lines) + break + else: + raise Exception("Got the key body when I wasn't expecting it") + elif state == State.CapLine: + capline = l + state = State.Header + else: + raise Exception("Could not parse key as public or private Mix Key:", l) + return key + +class PublicMixKey: + class State: + NoState = 0 + CapLine = 1 + Header = 2 + KeyID = 3 + Length = 4 + Key = 5 + Footer = 6 + Done = 7 + KeyId = 0 + KeyLen = 0 + KeyVal = "" + KeyVal_Decoded = 0 + Decoded_KeyLen = 0 + N = 0 + E = 0 + def __init__(self, lines=[]): + state = self.State.CapLine + + for l in lines: + l = l.strip() + + if not l: continue + elif l == "-----Begin Mix Key-----": + if state != self.State.Header: + print lines + raise Exception("Found Header when wasn't expecting it") + else: + state = self.State.KeyID + elif state == self.State.CapLine: + self.CapLine = l + state = self.State.Header + elif state == self.State.KeyID: + self.KeyId = l + state = self.State.Length + elif state == self.State.Length: + self.KeyLen = l + state = self.State.Key + elif l == "-----End Mix Key-----": + if state != self.State.Key: + raise Exception("Found Footer when wasn't expecting it") + else: + state = self.State.Done + elif state == self.State.Key: + self.KeyVal += l + else: + raise Exception("Got Non-Blank Line that doesn't fit.") + if lines: + self.decode() + def decode(self): + try: + peices = self.CapLine.split() + except AttributeError: + raise Exception("Could not parse Public Key without a CapLine:", self.KeyId) + + self.ShortName = peices[0] + self.Address = peices[1] + otherKeyId = peices[2] + if otherKeyId != self.KeyId: + raise Exception("Key IDs don't match.") + + temp = peices[3].partition(":") + if temp[2]: + self.Protocol = temp[0] + self.Version = temp[2] + else: + self.Version = temp[0] + self.Protocol = "2" #Assume + + if len(peices) > 4: + temp = peices[4] + else: + temp = "" + self.Middleman = "M" in temp + self.Compress = "C" in temp + self.News = temp.replace("M", "").replace("C", "") + + + self.KeyVal_Decoded = b64decode(self.KeyVal) + if len(self.KeyVal_Decoded) != 258: + raise Exception("KeyLength is non-standard") + + bytes128 = 'B' * 128 + + self.Decoded_KeyLen = struct.unpack('' + bytes128, self.KeyVal_Decoded[2:130])) #128 Bytes + self.E = bigEndian(struct.unpack('>' + bytes128, self.KeyVal_Decoded[130:])) #128 Bytes + + PublicRSAKey = None + def getPCPublicKey(self): + if not self.PublicRSAKey: + self.PublicRSAKey = RSA.construct((long(self.N), long(self.E))) + return self.PublicRSAKey + + def toMixFormat(self): + encodedData = struct.pack(' pyk.q else pyk.q + k.Q = pyk.q if pyk.p > pyk.q else pyk.p + k.DMP1 = modinv(k.E, k.P-1) + k.DMQ1 = modinv(k.E, k.Q-1) + k.IQMP = modinv(k.Q, k.P) + + encodedData = struct.pack('' + bytes128, self.KeyVal_Decrypted[2:130])) #128 Bytes + self.E = bigEndian(struct.unpack('>' + bytes128, self.KeyVal_Decrypted[130:258])) #128 Bytes + self.D = bigEndian(struct.unpack('>' + bytes128, self.KeyVal_Decrypted[258:386])) #128 Bytes + self.P = bigEndian(struct.unpack('>' + bytes64, self.KeyVal_Decrypted[386:450])) #64 Bytes + self.Q = bigEndian(struct.unpack('>' + bytes64, self.KeyVal_Decrypted[450:514])) #64 Bytes + self.DMP1 = bigEndian(struct.unpack('>' + bytes64, self.KeyVal_Decrypted[514:578])) #64 Bytes + self.DMQ1 = bigEndian(struct.unpack('>' + bytes64, self.KeyVal_Decrypted[578:642])) #64 Bytes + self.IQMP = bigEndian(struct.unpack('>' + bytes64, self.KeyVal_Decrypted[642:706])) #64 Bytes + + if self.N - (self.P * self.Q) != 0: + raise Exception("N - (P * Q) != 0") + + if self.P < self.Q: + raise Exception("P < Q") + phi = (self.P-1) * (self.Q-1) + myd = modinv(self.E, phi) + if myd != self.D: + raise Exception("Did not calculate D to be the value given in the private key file.") + mydmp1 = modinv(self.E, self.P-1) + if mydmp1 != self.DMP1: + raise Exception("Did not calculate DMP1 to be the value given in the private key file.") + mydmq1 = modinv(self.E, self.Q-1) + if mydmq1 != self.DMQ1: + raise Exception("Did not calculate DMQ1 to be the value given in the private key file.") + myiqmp = modinv(self.Q, self.P) + if myiqmp != self.IQMP: + raise Exception("Did not calculate IQMP to be the value given in the private key file.") + def getPublicMixKey(self): + key = PublicMixKey() + key.N = self.N + key.E = self.E + key.KeyId = self.KeyId + key.ShortName = self.ShortName + key.Address = self.Address + key.Protocol = self.Protocol + key.Version = self.Version + key.Middleman = self.Middleman + key.Compress = self.Compress + key.News = self.News + return key + + PrivateRSAKey = None + def getPCPrivateKey(self): + if not self.PrivateRSAKey: + self.PrivateRSAKey = RSA.construct((long(self.N), long(self.E), long(self.D), long(self.P), long(self.Q))) + return self.PrivateRSAKey + + PublicRSAKey = None + def getPCPublicKey(self): + if not self.PublicRSAKey: + self.PublicRSAKey = RSA.construct((long(self.N), long(self.E))) + return self.PublicRSAKey + + def pprint(self): + print " Created:", self.Created + print " Expires:", self.Expires + print " Key ID:", self.KeyId + + if not self.Passphrase: + print " Could not decrypt secret key" + else: + print "Shortname:", self.ShortName + print "Address :", self.Address + print "Version :", self.Version + print "Protocol :", self.Protocol + print "Middleman:", self.Middleman + print "Compress :", self.Compress + print "News :", self.News + print "Key Val :" #, keyval + print " Len :", self.Decoded_KeyLen + print " N :", self.N + print " E :", self.E + print " D :", self.D + print " P :", self.P + print " Q :", self.Q + print " DMP1 :", self.DMP1 + print " DMQ1 :", self.DMQ1 + print " IQMP :", self.IQMP + print self.getPublicMixKey().toMixFormat() + + def loadPublicCapabilities(self, key): + self.ShortName = key.ShortName + self.Address = key.Address + self.Protocol = key.Protocol + self.Version = key.Version + self.Middleman = key.Middleman + self.Compress = key.Compress + self.News = key.News + +if __name__ == "__main__": + logging.config.fileConfig("../../config/test_logging.conf") + lines = sys.stdin.readlines() + passphrase = "" + if len(sys.argv) > 1: + passphrase = sys.argv[1] + + k1 = parseMixKey(lines, passphrase) + k1.pprint() diff --git a/mixKeystore.py b/mixKeystore.py new file mode 100644 index 0000000..bbbcf31 --- /dev/null +++ b/mixKeystore.py @@ -0,0 +1,124 @@ +#!/usr/bin/python + +import subprocess +import logging, logging.config + +try: + from lamson.cron import getCronTab, CronEvent +except: + def getCronTab(): + return None + +from mixKey import * +from mixConfig import getRemailerConfig + +_mixKeyStore = None +def getKeyStore(): + global _mixKeyStore + if not _mixKeyStore: + _mixKeyStore = loadFreshKeystore() + + return _mixKeyStore + +def loadFreshKeystore(): + _mixKeyStore = MixKeyStore() + + # This assumes only one private key will be found in the file. + try: + f = open(getRemailerConfig()['filelocations']['secring.mix'], 'r') + except IOError: + f = open('../../' + getRemailerConfig()['filelocations']['secring.mix'], 'r') + privateKeyLines = f.readlines() + f.close() + _mixKeyStore.addKey(privateKeyLines, getRemailerConfig('remailerkeypassword')) + + try: + f = open(getRemailerConfig()['filelocations']['pubring.mix'], 'r') + except IOError: + f = open('../../' + getRemailerConfig()['filelocations']['pubring.mix'], 'r') + lines = f.readlines() + f.close() + key = [] + for l in lines: + l = l.strip() + if l == "-----End Mix Key-----": + key.append(l) + _mixKeyStore.addKey(key) + key = [] + else: + key.append(l) + return _mixKeyStore + +def refreshStats(): + devnull = open("/dev/null", "w") + subprocess.call(["wget", getRemailerConfig('statsdirectory') + "mlist.txt"], stderr=devnull) + subprocess.call(["wget", getRemailerConfig('statsdirectory') + "rlist.txt"], stderr=devnull) + subprocess.call(["wget", getRemailerConfig('statsdirectory') + "pubring.mix"], stderr=devnull) + subprocess.call(["wget", getRemailerConfig('statsdirectory') + "pgp-all.asc"], stderr=devnull) + subprocess.call(["mv", "mlist.txt", "rlist.txt", "pubring.mix", "pgp-all.asc", "app/data/"]) + devnull.close() + +def refreshKeyStore(): + global _mixKeyStore + _mixKeyStore = loadFreshKeystore() + +class MixKeyStore: + keystore = {} + def __init__(self): + cron = getCronTab() + if cron: + cron.add(CronEvent(refreshStats, 0, range(0,24))) + else: + logging.debug("No Cron Object found, cannot add refresh stats cron job") + def addKey(self, keylines, passphrase=""): + key = parseMixKey(keylines, passphrase) + + if key.KeyId in self.keystore: + if isinstance(self.keystore[key.KeyId], PublicMixKey) and isinstance(key, PrivateMixKey): + logging.info("Replacing Public Key " + key.KeyId + " in self.keystore with Private Key") + key.loadPublicCapabilities(self.keystore[key.KeyId]) + self.keystore[key.KeyId] = key + else: + self.keystore[key.KeyId].loadPublicCapabilities(key) + logging.info("Loading Public Key Properties of " + key.KeyId + " into Private Key in self.keystore ") + else: + self.keystore[key.KeyId] = key + logging.info("Adding Key " + key.KeyId + " to self.keystore") + def getKey(self, keyid): + if keyid in self.keystore: + return self.keystore[keyid] + else: + return None + def getPublicKey(self, keyid): + if keyid in self.keystore and isinstance(self.keystore[keyid], PrivateMixKey): + return self.keystore[keyid].getPublicMixKey() + elif keyid in self.keystore and isinstance(self.keystore[keyid], PublicMixKey): + return self.keystore[keyid] + else: + return None + def getPrivateKey(self, keyid): + if keyid in self.keystore and isinstance(self.keystore[keyid], PrivateMixKey): + return self.keystore[keyid] + else: + return None + def listPrivateKeys(self): + keys = [] + for k in self.keystore: + if isinstance(self.keystore[k], PrivateMixKey): + keys.append(self.keystore[k]) + return keys + def listPublicKeys(self): + keys = [] + for k in self.keystore: + if isinstance(self.keystore[k], PublicMixKey): + keys.append(self.keystore[k]) + else: + keys.append(self.getPublicKey(k)) + return keys + +if __name__ == "__main__": + logging.config.fileConfig("../../config/test_logging.conf") + keys = getKeyStore().listPublicKeys() + keys.sort() + for k in keys: + print k.getMixKeyHeader() diff --git a/mixMath.py b/mixMath.py new file mode 100644 index 0000000..af1f8b9 --- /dev/null +++ b/mixMath.py @@ -0,0 +1,92 @@ +import struct, time + +def mixTimestampFromBinary(binstr): + """ + Timestamp: A timestamp is introduced with the byte sequence (48, 48, 48, + 48, 0). The following two bytes specify the number of days since Jan 1, + 1970, given in little-endian byte order. + """ + ba = binaryToByteArray(binstr) + if len(ba) != 7 or ba[0] != 48 or ba[1] != 48 or ba[2] != 48 or ba[3] != 48 or ba[4] != 0: + raise Exception("Invalid string given to mixTimestampFromBinary " + str(ba)) + days = littleEndian(ba[5:7]) + result = time.ctime(days * (24 * 60 * 60)) + return result +def hexpad(str, len): + return hex(str)[2:-1].zfill(len) +def binaryToByteArray(str): + a = struct.unpack("B" * len(str), str) + return list(a) +def byteArrayToBinary(arr): + str="" + for b in arr: + str += chr(b) + return str +def arrayLeftPad(arr, desiredLen, char): + while len(arr) < desiredLen: + arr.insert(0, char) + return arr +def bigEndian(param): + if isinstance(param, list) or isinstance(param, tuple): + #Convert an array of bytes to an int + x = 0 + for b in param: + x = (x << 8) + b + return x + elif isinstance(param, long) or isinstance(param, int): + #Convert a long to a big-endian array of bytes + result = [] + while param: + result.append(param & 0xFF) + param >>= 8 + result.reverse() + return result + else: + raise Exception("Called bigEndian with an unknown type:", type(param)) + +def littleEndian(arr): + arr.reverse() + return bigEndian(arr) +def modinv(u, v): + """ Computes the inverse of u, mod v: u^-1 mod v """ + u1 = 1 + u3 = u + v1 = 0 + v3 = v + iter = 1; + while v3 != 0: + q = u3 / v3 + t3 = u3 % v3 + t1 = u1 + q * v1 + + u1 = v1 + v1 = t1 + u3 = v3 + v3 = t3 + + iter = -iter + + if u3 != 1: + raise Exception("Error getting modular inverse") + return 0 + + if iter < 0: + return v - u1 + else: + return u1 +def modpow(base, exponent, modulus): + result = 1 + while exponent > 0: + if exponent & 1 == 1: + result = (result * base) % modulus + exponent = exponent >> 1 + base = (base * base) % modulus + return result + +def splitToNPerLine(data, wrapat=40): + output = "" + while len(data) > wrapat: + output += data[:wrapat] + "\n" + data = data[wrapat:] + output += data + return output diff --git a/mixMessage.py b/mixMessage.py new file mode 100644 index 0000000..b760a74 --- /dev/null +++ b/mixMessage.py @@ -0,0 +1,209 @@ +#!/usr/bin/python + +import sys, struct, random +import hashlib +from base64 import * + +import logging, logging.config + +from mixMath import * +from mixConfig import * +from mixKeystore import * +from mixPacketType import * +from mixHeader import * +from mixPayload import * + + +class MixMessage: + class State: + Colons = 1 + Type = 2 + Header = 3 + Length = 4 + Digest = 5 + Packet = 6 + Footer = 7 + Done = 8 + Type = 0 + Length = 0 + Digest = 0 + Packet = "" + PacketType = 0 + def __init__(self, lines): + if isinstance(lines, str) or isinstance(lines, unicode): + lines = lines.split("\n") + + state = self.State.Colons + for l in lines: + l = l.strip() + + if not l: continue + elif l == "::": + if state != self.State.Colons: + raise Exception("Found Colons when wasn't expecting it") + else: + state = self.State.Type + elif state == self.State.Type: + self.Type = l + state = self.State.Header + elif l == "-----BEGIN REMAILER MESSAGE-----": + if state != self.State.Header: + raise Exception("Found Header when wasn't expecting it") + else: + state = self.State.Length + elif state == self.State.Length: + self.Length = l + state = self.State.Digest + elif state == self.State.Digest: + self.Digest = l + state = self.State.Packet + elif l == "-----END REMAILER MESSAGE-----": + if state != self.State.Packet: + raise Exception("Found Footer when wasn't expecting it") + else: + state = self.State.Done + elif state == self.State.Packet: + self.Packet += l + else: + raise Exception("Got Non-Blank Line that doesn't fit at state", state, l) + + def decode(self, ignoreDigestErrors=False): + self.Packet_Decoded = b64decode(self.Packet) + if len(self.Packet_Decoded) != 20480: + raise Exception("Length is non-standard:", len(self.Packet_Decoded)) + self.Digest_Decoded = b64decode(self.Digest) + + sanitycheck = hashlib.md5(self.Packet_Decoded).digest() + if not ignoreDigestErrors and sanitycheck != self.Digest_Decoded: + raise Exception("Did not hash message to it's Matching ID") + + self.Headers= [] + for i in range(20): + h = None + if i == 0: + h = ParsedMixHeader(self.Packet_Decoded[(512 * i) : (512 * (i+1))], ignoreDigestErrors) + + elif self.Headers[0].DecryptedHeader.PacketTypeId == MixPacketType.IntermediateHop: + h = IntermediateMixHeader(self.Packet_Decoded[(512 * i) : (512 * (i+1))], self.Headers[0].DecryptedHeader.TDesKey, self.Headers[0].DecryptedHeader.IVs[i-1]) + + if h: self.Headers.append(h) + + if self.Headers[0].DecryptedHeader.PacketTypeId == MixPacketType.FinalHop: + self.Payload = ParsedMixPayload(self.Packet_Decoded[512*20:], self.Headers[0].DecryptedHeader.TDesKey, self.Headers[0].DecryptedHeader.getPayloadIV()) + elif self.Headers[0].DecryptedHeader.PacketTypeId == MixPacketType.IntermediateHop: + self.Payload = IntermediateMixPayload(self.Packet_Decoded[512*20:], self.Headers[0].DecryptedHeader.TDesKey, self.Headers[0].DecryptedHeader.getPayloadIV()) + + if isinstance(self.Payload, ParsedMixPayload) and "null:" in self.Payload.DestinationFields: + self.PacketType = MixPacketType.DummyMessage + else: + self.PacketType = self.Headers[0].DecryptedHeader.PacketTypeId + + + def pprint(self): + print "Sender Type: ", self.Type + print "Msg Len: ", self.Length + print "Msg Headers:" + for h in self.Headers: + h.pprint() + self.Payload.pprint() + + def _buildNextHopMessage(self): + if self.PacketType != MixPacketType.IntermediateHop: + raise Exception("Called buildNextHopMessage when this is not an Intermediate Hope") + + messagedata = "" + for h in self.Headers[1:]: + if len(h.DecryptedData) != 512: + raise Exception("Header Data is not 512 bytes") + messagedata += h.DecryptedData + + #Append a final, random, 512 byte header + for i in range(512): messagedata += chr(random.randrange(256)) + + messagedata += self.Payload.DecryptedData + + output = "::" + "\n" + output += "Remailer-Type: tagging-attack-demo\n" + output += "\n" + output += "-----BEGIN REMAILER MESSAGE-----" + "\n" + output += "20480" + "\n" + output += b64encode(hashlib.md5(messagedata).digest()) + "\n" + messagedata = b64encode(messagedata) + output += splitToNPerLine(messagedata) + "\n" + output += "-----END REMAILER MESSAGE-----" + "\n" + + return output + def deliveryTo(self): + if self.PacketType == MixPacketType.IntermediateHop: + return self.Headers[0].DecryptedHeader.RemailerAddress + elif self.PacketType == MixPacketType.FinalHop: + return self.Payload.DestinationFields + elif self.PacketType == MixPacketType.DummyMessage: + return Exception("Called deliveryTo on a Dummy Message") + else: + raise Exception("Called deliveryTo with a PacketType that is unhandled") + def deliverySubject(self): + if self.PacketType == MixPacketType.IntermediateHop: + return ""#Subject doesn't matter + elif self.PacketType == MixPacketType.FinalHop: + return ' / '.join(self.Payload.getHeader('Subject')) + elif self.PacketType == MixPacketType.DummyMessage: + return Exception("Called deliverySubject on a Dummy Message") + else: + raise Exception("Called deliverySubject with a PacketType that is unhandled") + def deliveryBody(self): + if self.PacketType == MixPacketType.IntermediateHop: + return self._buildNextHopMessage() + elif self.PacketType == MixPacketType.FinalHop: + return self.Payload.UserData + elif self.PacketType == MixPacketType.DummyMessage: + return Exception("Called deliveryBody on a Dummy Message") + else: + raise Exception("Called deliveryBody with a PacketType that is unhandled") + def deliveryHeaders(self): + if self.PacketType == MixPacketType.IntermediateHop: + return [] + elif self.PacketType == MixPacketType.FinalHop: + results = [] + for field in self.Payload.HeaderFields: + h, sep, v = field.partition(':') + h = h.strip() + v = v.strip() + if getRemailerConfig().allowHeader(h, v): + results.append((h, v)) + return results + elif self.PacketType == MixPacketType.DummyMessage: + return Exception("Called deliveryHeaders on a Dummy Message") + else: + raise Exception("Called deliveryHeaders with a PacketType that is unhandled") + def messageid(self): + if self.PacketType == MixPacketType.FinalHop: + return self.Headers[0].DecryptedHeader.MessageId + else: + return Exception("Called messageid on a MixMessage that isn't a Final Hop") + + +if __name__ == "__main__": + logging.config.fileConfig("../../config/test_logging.conf") + + if len(sys.argv) > 1: + extrakeyfile = sys.argv[1] + extrakeypassphrase = sys.argv[2] + f = open(extrakeyfile, "r") + extrakeylines = f.readlines() + f.close() + getKeyStore().addKey(extrakeylines, extrakeypassphrase) + + lines = sys.stdin.readlines() + msg = MixMessage(lines) + msg.pprint() + print "========================" + if msg.PacketType == MixPacketType.DummyMessage: + print "Dummy Message" + else: + print msg.deliveryTo() + print msg.deliverySubject() + print msg.deliveryHeaders() + print msg.deliveryBody() + if msg.PacketType == MixPacketType.FinalHop: + print msg.messageid() \ No newline at end of file diff --git a/mixPacketType.py b/mixPacketType.py new file mode 100644 index 0000000..01124b3 --- /dev/null +++ b/mixPacketType.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +class MixPacketType: + IntermediateHop = 0 + FinalHop = 1 + FinalHopPartialMessage = 2 + DummyMessage = 1000 + @staticmethod + def toPretty(i): + if i == MixPacketType.IntermediateHop: return "IntermediateHop" + elif i == MixPacketType.FinalHop: return "FinalHop" + elif i == MixPacketType.FinalHopPartialMessage: return "FinalHopPartialMessage" + elif i == MixPacketType.DummyMessage: return "DummyMessage" + else: return "Hell if I know, strange value kid!" diff --git a/mixPayload.py b/mixPayload.py new file mode 100644 index 0000000..561bfeb --- /dev/null +++ b/mixPayload.py @@ -0,0 +1,130 @@ +#!/usr/bin/python + +import struct +from Crypto.Cipher import DES3 + +from mixMath import * + +class IntermediateMixPayload: + def __init__(self, data, decryptionkey=None, decryptioniv=None): + des = DES3.new(decryptionkey, DES3.MODE_CBC, IV=decryptioniv) + self.DecryptedData = des.decrypt(data) + def pprint(self): + print "\tIntermediate Payload Packet" + +class ParsedMixPayload: + class UserDataType: + Gzip = 1 + Email = 2 + Plain = 3 + Empty = 100 + @staticmethod + def toPretty(i): + if i == ParsedMixPayload.UserDataType.Gzip: return "Gzip" + elif i == ParsedMixPayload.UserDataType.Email: return "Email" + elif i == ParsedMixPayload.UserDataType.Plain: return "Plain" + elif i == ParsedMixPayload.UserDataType.Empty: return "Empty" + else: return "Hell if I know, strange value kid!" + DecryptionKey = 0 + IV = 0 + DecryptedData = 0 + + NumDestinationFields = 0 + DestinationFields = [] + NumHeaderFields = 0 + HeaderFields = [] + UserData = 0 + UserDataTypeId = 0 + def __init__(self, data, key, iv): + self.DecryptionKey = key + self.IV = iv + des = DES3.new(self.DecryptionKey, DES3.MODE_CBC, IV=self.IV) + self.DecryptedData = des.decrypt(data) + + self.DataLength = littleEndian(binaryToByteArray(self.DecryptedData[0:4])) + byteIndex = 4 + + self.DecryptedData = self.DecryptedData[0 : self.DataLength + 4] + + self.DestinationFields = [] + self.NumDestinationFields = struct.unpack('B', self.DecryptedData[byteIndex])[0] + byteIndex += 1 + for i in range(self.NumDestinationFields): + self.DestinationFields.append(self.DecryptedData[byteIndex : byteIndex + 80].strip(chr(0)).strip()) + byteIndex += 80 + + self.HeaderFields = [] + self.NumHeaderFields = struct.unpack('B', self.DecryptedData[byteIndex])[0] + byteIndex += 1 + for i in range(self.NumHeaderFields): + self.HeaderFields.append(self.DecryptedData[byteIndex : byteIndex + 80].strip(chr(0)).strip()) + byteIndex += 80 + + self.UserData = self.DecryptedData[byteIndex : ] + self.UserDataTypeId = 0 + + if self.UserData: + if self.UserData[0] == 31 and self.UserData[1] == 139: + self.UserDataTypeId = self.UserDataType.Gzip + raise Exception("Got a gzipped payload - don't know how to handle this") + elif self.UserData[0] == 35 and self.UserData[1] == 35 and self.UserData[2] == 13: + self.UserDataTypeId = self.UserDataType.Email + raise Exception("Got an email payload - don't know how to handle this") + else: + self.UserDataTypeId = self.UserDataType.Plain + self.UserData = self.UserData + else: + self.UserDataTypeId = self.UserDataType.Empty + self.UserData = "" + + # Quicksilver seems to send mail in a nonstandard fashion. It writes + # no Destination Fields, nor Header Fields. Instead it places a ## + # aand then the headers, a blank line, and the body. + + if self.UserData.strip().startswith("##"): + PossibleQSMessage = self.UserData.strip()[3:] + lines = PossibleQSMessage.split("\n") + + QSHeaders = [] + QSDestinations = [] + beginQSBody = 0 + for i in range(len(lines)): + print ">", lines[i] + if lines[i].strip(): + h, sep, v = lines[i].partition(':') + if h and v: + if h.strip() == "To": + QSDestinations.append(lines[i]) + else: + QSHeaders.append(lines[i]) + else: + beginQSBody = i + break + + self.HeaderFields = QSHeaders + self.NumHeaderFields = len(QSHeaders) + self.DestinationFields = QSDestinations + self.NumDestinationFields = len(QSDestinations) + self.UserData = "\n".join(lines[beginQSBody+1:]) + + def pprint(self): + print "\tPacket Body -----------------------------" + print "\t Data Length :", self.DataLength + print "\t Destination Fields:", self.NumDestinationFields + for d in self.DestinationFields: + print "\t ", d + print "\t Header Fields :", self.NumHeaderFields + for h in self.HeaderFields: + print "\t ", h + print "\t User Data Type :", self.UserDataType.toPretty(self.UserDataTypeId), "(" + str(self.UserDataTypeId) + ")" + print "\t User Data:" + if self.UserDataTypeId == self.UserDataType.Plain: + print "\t ", self.UserData.replace("\n", "\n\t ") + + def getHeader(self, searchfor): + results = [] + for field in self.HeaderFields: + h, sep, v = field.partition(':') + if h.strip().lower() == searchfor.strip().lower(): + results.append(v.strip()) + return results \ No newline at end of file