autonym.py 13.1 KB
Newer Older
putro's avatar
putro committed
1 2 3 4 5
#!/usr/bin/env python

import os
import sys
import checks
6
from config import config, nyms, nymList, printNymList, configSection, writeConfig, delSection
putro's avatar
putro committed
7 8 9 10
import traceback
import re
from stats import parse_stats, stats_m, uptime_sort_m, format_stats
from utils import pressKey, validateChoice, chooseList, askPassphrase, askSomething, askYesNo
11
from utils import chooseSubjType, selectServer, getDomain, validateEmail, isGpg, isAscii, X_is_running, editMessage
putro's avatar
putro committed
12
import gpgfuncts
13
import easygui
putro's avatar
putro committed
14 15
import string
import subprocess
16
import optparse
putro's avatar
putro committed
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40




class bcolors:
    """
    Define colors for outputs
    """
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    ENDC = '\033[0m'

    def disable(self):

        self.GREEN = ''
        self.YELLOW = ''
        self.RED = ''
        self.ENDC = ''


def menu():
    """ Print menu and wait for user choice """
    os.system(['clear', 'cls'][os.name == 'nt'])
41
    print bcolors.GREEN + "----- AUTONYM - for zax type nyms -----" + bcolors.ENDC
putro's avatar
putro committed
42 43 44 45 46 47 48 49 50 51 52
    if anym is False:
        print bcolors.RED + "active nym: NONE"
    else:
        print bcolors.GREEN + "active nym: %s " % anym.name
    if re.match("OK", config['last_message']):
        print bcolors.YELLOW + "\ndebug: %s\n" % config['last_message'] + bcolors.ENDC
    else:
        print bcolors.RED + "\ndebug: %s\n" % config['last_message'] + bcolors.ENDC
    selection = raw_input("Enter\n\
    1 to choose/select active nym\n\
    2 to write a message (as active nym)\n\
53 54
    3 to create a nym\n\
    4 to update a nym (i.e. set new subject...)\n\
putro's avatar
putro committed
55
    5 to send a request of deletion for a nym\n\
56
    6 to create a new secret key\n\
57
    7 to delete a nym secret key and local config\n\
58
    8 to save nym secret passphrase in config file (NOT SECURE)\n\
putro's avatar
putro committed
59 60
    q to quit\n\n\n")

61
    # perform actions based on selection above
putro's avatar
putro committed
62 63 64 65 66 67 68 69 70
    if selection == "1":
        askNym()
        menu()
    elif selection == "2":
        writeMessage()
        menu()
    elif selection == "3":
        createNym()
        menu()
71
    elif selection == "4":
72
        updateNym()
putro's avatar
putro committed
73
        menu()
74
    elif selection == "5":
75 76 77
        updateNym(delete=True)
        menu()
    elif selection == "6":
putro's avatar
putro committed
78 79 80 81 82 83 84 85
        print "\nsecret keys in your keyring:"
        gpgfuncts.listSecKeys()
        print "\n"
        gpgfuncts.createKey()
        print "key generated, secret keys in your keyring: "
        gpgfuncts.listSecKeys()
        pressKey()
        menu()
86
    elif selection == "7":
87
        deleteNymKeysConfig()
putro's avatar
putro committed
88
        menu()
89
    elif selection == "8":
90
        savePassword()
putro's avatar
putro committed
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
        menu()
    elif selection == "q":
        sys.exit("bye bye")
    else:
        print "wrong selection"
        pressKey()
        menu()

def askNym():
    """ set active nym """
    nymlist = nymList(nyms)
    setActiveNym(chooseList(nymlist))
    return


def createNym():
    name = askSomething('Enter the name of the nym (without domain): ')
    server = selectServer(config)
    email = name + "@" + server
putro's avatar
putro committed
110 111 112 113 114
    dest = "config@" + server

    if not gpgfuncts.selectKey(dest):
        config['last_message'] = "Server %s key not available, abort" % server
        menu()
putro's avatar
putro committed
115 116

    if not gpgfuncts.selectKey(email):
putro's avatar
putro committed
117
        print "no key available, creating a new one for %s" % email
putro's avatar
putro committed
118 119 120 121 122 123 124 125 126
        gpgfuncts.createKey(email, name)

    subj_type = chooseSubjType(config)
    subject = askSomething('Enter the subject you want to use with this nym: ')

    configSection(email, subject, subj_type)
    writeConfig()
    setActiveNym(email)

127
    msg = prepareSetupMsg(gpgfuncts.selectSecKey(email), subj_type, subject)
putro's avatar
putro committed
128
    emsg = signCryptMsg(msg, dest, sign=False, passphrase="")
putro's avatar
putro committed
129
    if not emsg:
putro's avatar
putro committed
130
        config['last_message'] = "Something went wrong while preparing the message, recipient key missing ???"
putro's avatar
putro committed
131 132 133
        menu()
    sendMixMsg(dest, emsg)

134 135
def updateNym(delete=False):
    print "Choose nym to update/delete:"
putro's avatar
putro committed
136 137
    askNym()
    dest = "config@" + anym.domain
putro's avatar
putro committed
138

putro's avatar
putro committed
139
    if not gpgfuncts.selectKey(dest):
putro's avatar
putro committed
140
        config['last_message'] = "Server %s key not available, abort" % dest
putro's avatar
putro committed
141 142 143
        menu()

    if not gpgfuncts.selectKey(anym.name):
144
        print "no key available for %s" % anym.name
putro's avatar
putro committed
145 146 147 148 149 150
        menu()

    if not anym['passphrase']:
        pwd = askPassphrase()
    else:
        pwd = anym['passphrase']
151

152 153
    if not delete:
        subj_type = chooseSubjType(config)
154 155 156 157 158 159 160 161
        while True:
            subject = askSomething('Enter the subject you want to use with this nym: ')
            if isAscii(subject):
                break
            else:
                print "ERROR: subject contains not ascii characters, try again"
                pressKey()
                continue
162 163 164 165 166 167 168

        configSection(anym.name, subject, subj_type)
        writeConfig()
        setActiveNym(anym.name)
    else:
        subj_type = "delete"
        subject = "yes"
putro's avatar
putro committed
169

170
    msg = prepareSetupMsg(gpgfuncts.selectSecKey(anym.name), subj_type, subject)
171

putro's avatar
putro committed
172 173 174 175 176
    emsg = signCryptMsg(msg, dest, sign=anym.fp, passphrase=pwd)
    if not emsg:
        config['last_message'] = "Something went wrong while preparing the message, recipient key missing ???"
        menu()
    sendMixMsg(dest, emsg)
putro's avatar
putro committed
177 178


179 180 181 182 183 184 185 186
def savePassword():
    askNym()
    pwd = askPassphrase()
    configSection(anym.name, passphrase=pwd)
    writeConfig()
    config['last_message'] = "Password for %s saved in config.ini file (not secure, use just for testing)"


187 188 189 190 191 192 193 194 195 196 197
def deleteNymKeysConfig():
    askNym()
    if not askYesNo('Are you sure you want to delete local config file and %s secret key ?: ' % anym.name):
        menu()
    gpgfuncts.deleteKey(anym.fp)
    msg = "key and config of %s deleted" % anym.name
    delSection(anym.name)
    writeConfig()
    config['last_message'] = msg


putro's avatar
putro committed
198 199
def prepareSetupMsg(keyid, subj_type, subject):
    key = gpgfuncts.exportPubKey(keyid)
200 201 202 203 204 205 206 207
    try:
        msg = "\n%s: %s\n\n" % (subj_type, subject) + key
        return msg
    except UnicodeDecodeError, e:
        print 'Unicode Error: ', e.reason
        print 'Please use only plain ascii characters'
        pressKey()
        menu()
putro's avatar
putro committed
208 209


putro's avatar
putro committed
210 211
def signCryptMsg(msg, dest, sign, passphrase):
    msg = gpgfuncts.stripVersion(str(gpgfuncts.encrypt(msg, dest, sign=sign, passphrase=passphrase)))
212 213
    if msg == "ERROR":
        menu()
putro's avatar
putro committed
214 215 216 217 218 219 220 221 222 223 224 225 226 227
    return msg


def setActiveNym(nym):
    global anym
    anym = config['nym'][nym]
    anym.domain = getDomain(anym.name)
    anym.fp = gpgfuncts.selectSecKey(anym.name)


class MixError(Exception):
    pass


228
def sendMixMsg(dest, msg, subj="subject", test=False, chain=""):
putro's avatar
putro committed
229 230
    """ send msg through a local mixmaster client """
    args = [config['options']['mixmaster'], '--mail', '--to=' + dest, '--subject=' + subj, '--copies=1']
231
    chooseChain(chain=chain)
putro's avatar
putro committed
232 233 234 235 236 237 238 239
    args.append('--chain=' + ','.join(config['remailer_chain']))

    try:
        mix = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = mix.communicate(msg)
        if err.find('Error') >= 0:
            raise MixError('Mixmaster process returned the following error: ' + str(err) + '. Sending failed.')
        config['last_message'] = "OK, succesfully sent email"
240
    except OSError:    # usually means that mixmaster could not be executed
putro's avatar
putro committed
241 242 243 244
        raise MixError('Could not find mixmaster binary.')



245
def writeMessage(subject="", recipient="", text="", test=0):
putro's avatar
putro committed
246 247 248 249 250 251 252 253
    """
    Create a message to be sent from nym
    The message has to be sent through a remailer chain
    to send@nym-server-address
    """
    if anym.fp is False:
        config['last_message'] = "ERROR - select your nym, actual selection HAS NOT a secret key"
        menu()
254

255 256 257 258 259 260 261 262 263
    if subject == "":
        while True:
            subject = askSomething('Insert message subject: ')
            if isAscii(subject):
                break
            else:
                print "ERROR: subject contains not ascii characters, try again"
                pressKey()
                continue
putro's avatar
putro committed
264

265 266
    if recipient == "":
        recipient = raw_input("Insert recipient email address or newsgroup name: ")
putro's avatar
putro committed
267

268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
    if text == "":
        if X_is_running():
            text = easygui.textbox(msg='Write your message....', title='Input Box', text='', codebox=0)
        else:
            text = editMessage()
        text = filter(lambda x: x in string.printable, text)
        if isGpg(text):
            print "message already encrypted"
            pressKey()
        elif gpgfuncts.encrypt("test", recipient, test=True).status == "encryption ok":
            # really bad fix to solve a ridiculous problem, first line gets removed...  to be investigated
            text = "Message:\n" + text
            text = gpgfuncts.encrypt(text, recipient)
            print "message has been encrypted with %s public key" % recipient
            pressKey()
        else:
            print "msg has not been encrypted, public key for %s not found" % recipient
            pressKey()
putro's avatar
putro committed
286 287 288 289 290

    msg = "From: %s\n" % anym.name
    if validateEmail(recipient):
        msg = msg + "To: %s\n" % recipient
    else:
291
        msg = msg + "To: %s\n" % [config['options']['mail-to-news']]
putro's avatar
putro committed
292 293 294 295 296 297 298 299 300 301 302
        msg = msg + "Newsgroups: %s\n" % recipient
    msg = msg + "Subject: %s\n\n" % subject

    if not anym['passphrase']:
        pwd = askPassphrase()
    else:
        pwd = anym['passphrase']

    msg = msg + text
    dest = 'send@' + anym.domain

303
    emsg = signCryptMsg(msg, dest, sign=anym.fp, passphrase=pwd)
putro's avatar
putro committed
304

305 306 307 308
    if test == 1:
        sendMixMsg(dest, emsg, chain="paranoia")
    else:
        sendMixMsg(dest, emsg)
putro's avatar
putro committed
309 310 311 312 313 314


def modifyConfig():
    pass


315
def chooseChain(chain=""):
putro's avatar
putro committed
316
    """ Print remailer list and prompt for chain choice """
317 318 319 320
    if chain != "":
        config['remailer_chain'] = [chain]
        return
    print "\nChoose remailer chain used to send the message to the nymserver\n"
putro's avatar
putro committed
321 322 323 324 325
    for remailer in uptime_sort_m():
        print format_stats(remailer)
    chain = raw_input('Choose remailer to be chained (by num), separated by commas, or write "r" for a random chain (3 remailers): ')
    if chain == "r":
        config['remailer_chain'] = ['*','*','*']
326
        return
putro's avatar
putro committed
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
    else:
        validateChain(chain)


def validateChain(chain):
    """
    Validate remailer chain, check the availability of public key of remailers
    and that last remailer of chain is not middleman
    """
    remailer_chain = []
    comma = re.compile(',')

    if len(chain.split(',')) > 1:
        if not comma.search(chain):
            print bcolors.RED + "ERROR - remailer chain selections have to be separated by commas" + bcolors.ENDC
            pressKey()
            return
    a = chain.split(',')
    for n in a:
        if not n.isdigit():
            print bcolors.RED + "ERROR - remailer chain selection cannot contains non-numeric chars" + bcolors.ENDC
            return
        n = int(n)
        n -= 1
        remailer_chain.append(uptime_sort_m()[n])

    # check if last remailer is middleman
    if checkMiddle(remailer_chain[len(remailer_chain)-1]):
        print bcolors.RED + "ERROR - remailer %s is middleman, it cannot be last remailer in chain" % remailer_chain[len(remailer_chain)-1] + bcolors.ENDC
        pressKey()
        chooseChain()
    if checkBrokenChains(remailer_chain):
        print bcolors.RED + "ERROR - broken chain selected, check the breaks in stats last column" + bcolors.ENDC
        pressKey()
        chooseChain()

    config['last_message'] = "OK, remailer chain choosed: %s" % remailer_chain
    config['remailer_chain'] = remailer_chain


def checkMiddle(remailer):
    """ check if a remailer is middleman """
    if stats_m[remailer].middleman:
        return True
    else:
        return False

def checkBrokenChains(remailer_chain):
    """ check for broken remailer chains """
    for i in range(0,len(remailer_chain)-1):
        cur = remailer_chain[i]
        next = remailer_chain[i+1]
        if next in stats_m[cur].broken:
            return True
        else:
            return False


385
def main():
putro's avatar
putro committed
386

387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
    if checks.OptionsCheck(config) == True:
        config['last_message'] = "OK, initial check passed"

    if not nyms.sections:
        print "No nyms configured, going to create it..."
        createNym()
    else:
        setActiveNym(nyms.sections[0])

    parser = optparse.OptionParser()
    parser.add_option('--test', action='store_true', dest='test')
    opts, args = parser.parse_args()

    try:
        if opts.test:
402 403 404 405 406 407 408 409
            if config['options']['testnym'] == "":
                print bcolors.RED + "\nTestnym not configured in config.ini, exiting..." + bcolors.ENDC
                sys.exit()
            else:
                setActiveNym(config['options']['testnym'])
                parse_stats()
                writeMessage(subject="prova nymphet", recipient=config['options']['testnym_recipient'], text="asdadsfj", test=1)
                print bcolors.GREEN + "\nTest message sent from nym %s to %s" % (config['options']['testnym'], config['options']['testnym_recipient'])
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
        else:
            parse_stats()
            menu()

    except KeyboardInterrupt:
        print "program terminated"
    except SystemExit:
        print "program terminated, bye"
    except:
        print bcolors.RED + "\nAn unhandled exception occured, here's the traceback!\n" + bcolors.ENDC
        traceback.print_exc()
        print bcolors.RED + "\nReport this to putro@autistici.org" + bcolors.ENDC
        sys.exit()

if __name__ == '__main__':
    main()