summaryrefslogtreecommitdiff
path: root/mumble/gencerthash.py
blob: 27e21afc84e9d1bdb3c476cb92b00697b330734b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#!/usr/bin/env python3

# TODO: can we use struct instead for blobParser?

import argparse
import getpass
import hashlib
import re
import sys
import os
from collections import defaultdict
try:
    import OpenSSL  # "python-pyopenssl" package on Arch
except ImportError:
    exit('You need to install PyOpenSSL ("pip3 install --user PyOpenSSL" if pip3 is installed)')

## DEFINE SOME PRETTY STUFF ##
class color(object):
    # Windows doesn't support ANSI color escapes like sh does.
    if sys.platform == 'win32':
        # Gorram it, Windows.
        # https://bugs.python.org/issue29059
        # https://bugs.python.org/issue30075
        # https://github.com/Microsoft/WSL/issues/1173
        import subprocess
        subprocess.call('', shell=True)
    PURPLE = '\033[95m'
    CYAN = '\033[96m'
    DARKCYAN = '\033[36m'
    BLUE = '\033[94m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'
    END = '\033[0m'

class Hasher(object):
    def __init__(self, args):
        self.args = args
        self.blobGetter(self.args['cert'])
        self.blobParser()

    def getPass(self):
        # Do we need to get the passphrase?
        if self.args['passphrase']:
            if self.args['passphrase'] == 'stdin':
                self.args['passphrase'] = sys.stdin.read().replace('\n', '')
            elif self.args['passphrase'] == 'prompt':
                _colorargs = (color.BOLD, color.RED, self.args['cert'], color.END)
                _repeat = True
                while _repeat == True:
                    _pass_in = getpass.getpass(('\n{0}What is the encryption password ' +
                                                'for {1}{2}{0}{3}{0} ?{3} ').format(*_colorargs))
                    if not _pass_in or _pass_in == '':
                        print(('\n{0}Invalid passphrase for {1}{2}{0}{3}{0} ; ' +
                               'please enter a valid passphrase!{3} ').format(*_colorargs))
                    else:
                        _repeat = False
                        self.args['passphrase'] = _pass_in.replace('\n', '')
                        print()
            else:
                self.args['passphrase'] = None
        return()

    def importCert(self):
        self.getPass()
        # Try loading the certificate
        try:
            self.pkcs = OpenSSL.crypto.load_pkcs12(self.cert, self.args['passphrase'])
        except OpenSSL.crypto.Error:
            exit('Could not load certificate! (Wrong passphrase? Wrong file?)')
        return()

    def hashCert(self):
        self.crt_in = self.pkcs.get_certificate()
        self.der = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1,
                                                   self.crt_in)
        self.hash = hashlib.sha1(self.der).hexdigest().lower()
        return(self.hash)

    def blobGetter(self, blobpath):
        self.cert = None
        self.blob = None
        _blst = blobpath.split(':')
        if len(_blst) == 2:
            blob = _blst[1]
            self.certtype = _blst[0].lower()
        elif len(_blst) == 1:
            blob = _blst[0]
            self.certtype = 'file'
        else:
            raise ValueError('{0} is not a supported path'.format(blobpath))
            self.certtype = None
        if self.certtype:
            _hexblob = None
            if self.certtype in ('plist', 'ini', 'file'):
                blob = os.path.abspath(os.path.expanduser(blob))
                if not os.path.isfile(blob):
                    raise FileNotFoundError('{0} does not exist'.format(blob))
            if self.certtype == 'reg':  # Only supported on Windows machines, obviously.
                if sys.platform == 'win32':
                    import winreg
                elif sys.platform == 'cygwin':
                    # https://bitbucket.org/sfllaw/cygwinreg/issues/5/support-python3
                    exit(('Python 3 under Cygwin does not support reading the registry. ' +
                          'Please use native-Windows Python 3 (for now) or ' +
                          'specify an actual PKCS #12 certificate file.'))
                    #try:
                    #    import cygwinreg as winreg
                    #except ImportError:
                    #    exit('You must install the cygwinreg python module in your cygwin environment to read the registry.')
                _keypath = blob.split('\\')
                _hkey = getattr(winreg, _keypath[0])
                _skeypath = _keypath[1:-1]
                _ckey = _keypath[-1]
                _r = winreg.OpenKey(_hkey, '\\'.join(_skeypath))
                _hexblob, _ = winreg.QueryValueEx(_r, _ckey)
                winreg.CloseKey(_r)
            elif self.certtype == 'plist':  # plistlib, however, is thankfully cross-platform.
                import plistlib
                with open(blob, 'rb') as f:
                    _pdata = plistlib.loads(f.read())
                    _hexblob = _pdata['net.certificate']
            elif self.certtype == 'ini':
                import configparser
                _parser = configparser.RawConfigParser()
                _parser.read(blob)
                _cfg = defaultdict(dict)
                for s in _parser.sections():
                    _cfg[s] = {}
                    for k in _parser.options(s):
                        _cfg[s][k] = _parser.get(s, k)
                self.blob = _cfg['net']['certificate']
            else:  # It's (supposedly) a PKCS #12 file - obviously, cross-platform.
                with open(blob, 'rb') as f:
                    self.cert = f.read()
        return()

    def blobParser(self):
        if not self.blob:
            return()
        if self.blob == '':
            raise ValueError('We could not find an embedded certificate.')
        # A pox upon the house of Mumble for not using base64. A POX, I SAY.
        # So instead we need to straight up de-byte-array the mess.
        # The below is an eldritch horror, bound to twist the mind of any sane man
        # into the depths of madness.
        # I probably might have been able to use a struct here, but meh.
        blob = re.sub('^"?@ByteArray\(0(.*)\)"?$',
                      '\g<1>',
                      self.blob,
                      re.MULTILINE, re.DOTALL)
        _bytes = b'0'
        for s in blob.split('\\x'):
            if s == '':
                continue
            _chunk = list(s)
            # Skip the first two chars for string interpolation - they're hex.
            _start = 2
            try:
                _hex = ''.join(_chunk[0:2])
                _bytes += bytes.fromhex(_hex)
            except ValueError:
                # We need to zero-pad, and alter the starting index
                # because yep, you guessed it - their bytearray hex vals
                # (in plaintext) aren't zero-padded, either.
                _hex = ''.join(_chunk[0]).zfill(2)
                _bytes += bytes.fromhex(_hex)
                _start = 1
            # And then append the rest as-is. "Mostly."
            # Namely, we need to change the single-digit null byte notation
            # to actual python null bytes, and then de-escape the escapes.
            # (i.e. '\t' => '   ')
            _str = re.sub('\\\\0([^0])',
                          '\00\g<1>',
                          ''.join(_chunk[_start:])).encode('utf-8').decode('unicode_escape')
            _bytes += _str.encode('utf-8')
            self.cert = _bytes
        return()

def parseArgs():
    # Set the default cert path
    _certpath = '~/Documents/MumbleAutomaticCertificateBackup.p12'
    # This catches ALL versions of macOS/OS X.
    if sys.platform == 'darwin':
        _cfgpath = 'PLIST:~/Library/Preferences/net.sourceforge.mumble.Mumble.plist'
    # ALL versions of windows, even Win10, on x86. Even 64-bit. I know.
    # And Cygwin, which currently doesn't even suppport registry reading (see blobGetter()).
    elif sys.platform in ('win32', 'cygwin'):
        _cfgpath = r'REG:HKEY_CURRENT_USER\Software\Mumble\Mumble\net\certificate'
    elif (sys.platform == 'linux') or (re.match('.*bsd.*', sys.platform)):  # duh
        _cfgpath = 'INI:~/.config/Mumble/Mumble.conf'
    else:
        # WHO KNOWS what we're running on
        _cfgpath = None
    if not os.path.isfile(os.path.abspath(os.path.expanduser(_certpath))):
        _defcrt = _cfgpath
    else:
        _defcrt = 'FILE:{0}'.format(_certpath)
    args = argparse.ArgumentParser()
    args.add_argument('-p',
                      '--passphrase',
                      choices = ['stdin', 'prompt'],
                      dest = 'passphrase',
                      default = None,
                      help = ('The default is to behave as if your certificate does not have ' +
                              'a passphrase attached (as this is Mumble\'s default); however, ' +
                              'if you specify \'stdin\' we will expect the passphrase to be given as a stdin pipe, ' +
                              'if you specify \'prompt\', we will prompt you for a passphrase (it will not be echoed back' +
                              'to the console)'))
    args.add_argument('-c', '--cert',
                      dest = 'cert',
                      default = _defcrt,
                      metavar = 'path/to/mumblecert.p12',
                      help = ('The path to your exported PKCS #12 Mumble certificate. ' +
                              'Special prefixes are ' +
                              '{0} (it is a PKCS #12 file, default), ' +
                              '{1} (it is embedded in a macOS/OS X PLIST file), ' +
                              '{2} (it is a Mumble.conf with embedded PKCS#12), or ' +
                              '{3} (it is a path to a Windows registry object). ' +
                              'Default: {4}').format('{0}FILE{1}'.format(color.BOLD, color.END),
                                                     '{0}PLIST{1}'.format(color.BOLD, color.END),
                                                     '{0}INI{1}'.format(color.BOLD, color.END),
                                                     '{0}REG{1}'.format(color.BOLD, color.END),
                                                     '{0}{1}{2}'.format(color.BOLD, _defcrt, color.END)))
                                                     # this  ^ currently prints "0m" at the end of the help message,
                                                     # all the way on the left on Windows.
                                                     # Why? Who knows; Microsoft is a mystery even to themselves.
    return(args)

def main():
    args = vars(parseArgs().parse_args())
    cert = Hasher(args)
    cert.importCert()
    h = cert.hashCert()
    print(('\n\t{0}Your certificate\'s public hash is: ' +
           '{1}{2}{3}\n\n\t{0}Please provide this to the Mumble server administrator ' +
           'that has requested it.{3}').format(color.BOLD, color.BLUE, h, color.END))

if __name__ == '__main__':
    main()