2017-10-12 02:12:36 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2017-11-17 14:57:58 -05:00
|
|
|
# TODO: can we use struct instead for blobParser?
|
|
|
|
|
2017-10-12 02:12:36 -04:00
|
|
|
import argparse
|
|
|
|
import getpass
|
|
|
|
import hashlib
|
2017-11-17 14:57:58 -05:00
|
|
|
import re
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
from collections import defaultdict
|
2017-10-12 02:12:36 -04:00
|
|
|
try:
|
|
|
|
import OpenSSL # "python-pyopenssl" package on Arch
|
|
|
|
except ImportError:
|
|
|
|
exit('You need to install PyOpenSSL ("pip3 install --user PyOpenSSL" if pip3 is installed)')
|
2017-11-17 14:57:58 -05:00
|
|
|
|
|
|
|
## DEFINE SOME PRETTY STUFF ##
|
|
|
|
class color(object):
|
|
|
|
# Windows doesn't support ANSI color escapes like sh does.
|
|
|
|
if sys.platform == 'win32':
|
2017-11-17 15:09:03 -05:00
|
|
|
# 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'
|
2017-10-12 02:12:36 -04:00
|
|
|
|
|
|
|
class Hasher(object):
|
|
|
|
def __init__(self, args):
|
|
|
|
self.args = args
|
2017-11-17 14:57:58 -05:00
|
|
|
self.blobGetter(self.args['cert'])
|
|
|
|
self.blobParser()
|
|
|
|
|
2017-10-12 02:12:36 -04:00
|
|
|
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':
|
2017-11-17 14:57:58 -05:00
|
|
|
_colorargs = (color.BOLD, color.RED, self.args['cert'], color.END)
|
2017-10-12 02:12:36 -04:00
|
|
|
_repeat = True
|
|
|
|
while _repeat == True:
|
2017-11-17 14:57:58 -05:00
|
|
|
_pass_in = getpass.getpass(('\n{0}What is the encryption password ' +
|
|
|
|
'for {1}{2}{0}{3}{0} ?{3} ').format(*_colorargs))
|
2017-10-12 02:12:36 -04:00
|
|
|
if not _pass_in or _pass_in == '':
|
2017-11-17 14:57:58 -05:00
|
|
|
print(('\n{0}Invalid passphrase for {1}{2}{0}{3}{0} ; ' +
|
|
|
|
'please enter a valid passphrase!{3} ').format(*_colorargs))
|
2017-10-12 02:12:36 -04:00
|
|
|
else:
|
|
|
|
_repeat = False
|
|
|
|
self.args['passphrase'] = _pass_in.replace('\n', '')
|
|
|
|
print()
|
|
|
|
else:
|
|
|
|
self.args['passphrase'] = None
|
|
|
|
return()
|
|
|
|
|
|
|
|
def importCert(self):
|
2017-11-17 14:57:58 -05:00
|
|
|
self.getPass()
|
2017-10-12 02:12:36 -04:00
|
|
|
# Try loading the certificate
|
|
|
|
try:
|
2017-11-17 14:57:58 -05:00
|
|
|
self.pkcs = OpenSSL.crypto.load_pkcs12(self.cert, self.args['passphrase'])
|
2017-10-12 02:12:36 -04:00
|
|
|
except OpenSSL.crypto.Error:
|
2017-11-17 14:57:58 -05:00
|
|
|
exit('Could not load certificate! (Wrong passphrase? Wrong file?)')
|
2017-10-12 02:12:36 -04:00
|
|
|
return()
|
|
|
|
|
|
|
|
def hashCert(self):
|
2017-11-17 14:57:58 -05:00
|
|
|
self.crt_in = self.pkcs.get_certificate()
|
2017-10-12 02:12:36 -04:00
|
|
|
self.der = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1,
|
2017-11-17 14:57:58 -05:00
|
|
|
self.crt_in)
|
2017-10-12 02:12:36 -04:00
|
|
|
self.hash = hashlib.sha1(self.der).hexdigest().lower()
|
|
|
|
return(self.hash)
|
2017-11-17 14:57:58 -05:00
|
|
|
|
|
|
|
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()
|
2017-10-12 02:12:36 -04:00
|
|
|
|
|
|
|
def parseArgs():
|
2017-11-17 14:57:58 -05:00
|
|
|
# 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.
|
2017-11-17 15:09:03 -05:00
|
|
|
# And Cygwin, which currently doesn't even suppport registry reading (see blobGetter()).
|
|
|
|
elif sys.platform in ('win32', 'cygwin'):
|
2017-11-17 14:57:58 -05:00
|
|
|
_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)
|
2017-10-12 02:12:36 -04:00
|
|
|
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)'))
|
2017-11-17 14:57:58 -05:00
|
|
|
args.add_argument('-c', '--cert',
|
|
|
|
dest = 'cert',
|
|
|
|
default = _defcrt,
|
2017-10-12 02:12:36 -04:00
|
|
|
metavar = 'path/to/mumblecert.p12',
|
2017-11-17 14:57:58 -05:00
|
|
|
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)))
|
2017-11-17 15:09:03 -05:00
|
|
|
# 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.
|
2017-10-12 02:12:36 -04:00
|
|
|
return(args)
|
|
|
|
|
|
|
|
def main():
|
|
|
|
args = vars(parseArgs().parse_args())
|
|
|
|
cert = Hasher(args)
|
|
|
|
cert.importCert()
|
|
|
|
h = cert.hashCert()
|
2017-11-17 15:09:03 -05:00
|
|
|
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))
|
2017-10-12 02:12:36 -04:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2017-11-17 14:57:58 -05:00
|
|
|
main()
|