def generateSessionKeyV1(password, lmhash, nthash): if POW: hash = POW.Digest(POW.MD4_DIGEST) else: hash = MD4.new() hash.update(NTOWFv1(password, lmhash, nthash)) return hash.digest()
def compute_nthash(password): # This is done according to Samba's encryption specification (docs/html/ENCRYPTION.html) password = unicode(password).encode('utf_16le') if POW: hash = POW.Digest(POW.MD4_DIGEST) else: hash = MD4.new() hash.update(password) return hash.digest()
def sign(self, plaintext): """Sign the message with our private key. Returns the base64 encoded signature.""" if not self.priv: self.readKeys() digest = POW.Digest(POW.MD5_DIGEST) digest.update(plaintext) return base64.encodestring( self.priv.sign(digest.digest(), POW.MD5_DIGEST))
def compute_nthash(password): # This is done according to Samba's encryption specification (docs/html/ENCRYPTION.html) try: password = unicode(password).encode('utf_16le') except UnicodeDecodeError: import sys password = password.decode(sys.getfilesystemencoding()).encode('utf_16le') if POW: hash = POW.Digest(POW.MD4_DIGEST) else: hash = MD4.new() hash.update(password) return hash.digest()
class Service411: """Can retrieve a 411 file from a set of master servers. Has the ability to encrypt, and decrypt files using a hybrid RSA-Symmetric cryptographic technique. Only as safe as your sysadmin.""" def __init__(self): self.priv_filename = '/etc/411-security/master.key' self.pub_filename = '/etc/411-security/master.pub' self.shared_filename = '/etc/411-security/shared.key' self.masters = [] # Our current favorite self.master = None self.disable = 0 self.verbose = 0 # The directory from which we place our 411 files. Used # when translating 411 file paths. self.rootdir = "/" self.sym = None self.pool = None # Master keys self.priv = None self.pub = None # Shared key, 256bit + 64bit IV self.shared = None self.conn = None # Default URL base path to find files. self.urldir = '411.d' # Groups we are interested in self.groups = [''] # Store attributes which we can use to filter on. self.attrs = {} self.config = Conf(self) self.config.parse() self.plugin = None # A regex for our header search. pattern = "\n*(?P<comment>.*?)\$411id\$" self.header_pattern = re.compile(pattern) pattern = "<a href=.+>(?P<filename>.+)</a> +(?P<date>\d+.*) +(?P<size>\d+.*)" # Make the pattern matching engine case-insensitive. self.dir_pattern = re.compile(pattern, re.I) # Use Blowfish with fast Cipher Block Chaining. self.sym = POW.Symmetric(POW.BF_CBC) def fillPool(self): """Starts the random pool. Dont do in constructor since this is computationally intensive.""" # A cryptographically safe source of random data. self.pool = RandomPool(384) def setConf(self, conf): self.config = conf(self) def setConfHandler(self, handler): self.config.setHandler(handler) self.config.parse() def four11Path(self, filename411): """Translates 411 file names into UNIX absolute filenames.""" # Warning this is UNIX dependant n = string.replace(filename411, ".", "/") n = string.replace(n, "//", ".") return os.path.normpath(os.path.join(self.rootdir, n)) def path411(self, filename): """Turns an absolute UNIX path into a 411 filename. Every period ('.') is a filesystem delimeter (like /), and all filenames are assumed to be absolute, to start with a /. A literal period is coded as a double period ('..'). """ # Warning, this is UNIX dependant. n = string.replace(filename[1:], ".", "..") n = string.replace(n, "/", ".") return n def connect(self, master=None): """Opens a HTTP 1.1 connection to the first live master server. Will use the master argument if specified, otherwise consults internal master server list. This connection can service multiple requests.""" if self.conn: self.disconnect() if master: masters = [master] else: masters = self.getMasters() if not masters: raise ValueError, \ "We have no master servers to connect to." conn = None for master in masters: # Split address into host & port m = master.getAddress().split(':') if len(m) == 2: conn = HTTPConnection(m[0], int(m[1])) else: conn = HTTPConnection(m[0]) # Test the connection. try: # Set variable so that connection originates # from privileged port only. This way, only # root can initialize this connection conn.privileged_port = True conn.connect() except: # If we cannot connect, devalue this server. master.decScore() conn = None continue else: # We pick the first master that accepts a # connection. self.master = master master.incScore() break if conn: self.conn = conn else: raise Error411, \ "Could not reach a master server. Masters: %s" \ % masters def disconnect(self): """Closes the master HTTP connection. """ if self.conn: self.conn.close() self.conn = None def get(self, file): """ Retrives a 411 file. If arg is empty, a directory listing dictionary is returned. If file is valid, returns the decrypted contents and its associated meta data. """ if not file: raise Error411, "I need a file to get" # Allow you to get a full URL. if file.count("http://"): m = Master(file) self.connect(m) file = os.path.basename(file) else: # We use our internal configuration. self.connect() path = self.master.getDir() + file self.conn.request('GET', urllib.quote(path)) headers = self.conn.getresponse() status = headers['status'] reason = headers['reason'] if status != 200: raise Error411, "Could not get file '%s%s': %s %s" % \ (self.master.getUrl(), file, status, reason) contents = self.conn.read() self.disconnect() if file[-1] == '/': # A directory return contents else: return self.decrypt(contents) def find(self, path="/"): """Finds all relevant files to retrieve on a master server. Returns dict indexed by file path containing mtime and size.""" self.files = {} self.findHelper(path) return self.files def findHelper(self, path, depth=0): listing = self.get(path) lines = listing.split("\n") for line in lines: m = self.dir_pattern.search(line) if not m: continue #print line filename = m.group('filename') if filename == "Parent Directory": continue if filename[-1] == '/': if self.verbose: print "Found directory %s (%s)" \ % (path+filename, depth) if self.isInteresting(path + filename, depth): self.findHelper(path + filename, depth + 1) continue date = m.group('date').strip() size = m.group('size').strip() self.files[path + filename] = { "Name": filename, "Modified": date, "Size": size } def isInteresting(self, path, level): """Matches an offered group our registered groups. Slow, search time is squared with the number of registered groups. """ offered = path[1:-1].split('/') elements = len(offered) for r in self.groups: if not r: continue if self.verbose: print "Matching %s to %s level %s" \ % (offered, r[:len(offered)], level) try: # Shorten the registered group to match # the offerred one, then compare. Allows # group inheritance. if offered == r[:len(offered)]: return 1 except: continue return 0 def readKeys(self): """Loads the 411 shared and master RSA keys""" pub_file = open(self.pub_filename, 'r') self.pub = POW.pemRead(POW.RSA_PUBLIC_KEY, pub_file.read()) pub_file.close() shared_file = open(self.shared_filename, 'r') self.shared = self.readSharedKey(shared_file.read()) shared_file.close() if os.path.exists(self.priv_filename): priv_file = open(self.priv_filename, 'r') self.priv = POW.pemRead(POW.RSA_PRIVATE_KEY, priv_file.read()) priv_file.close() def makeSharedKey(self): """Uses our cryptographically safe random number generator to give us a 256bit session key and 64bit Init Vector, in 411 format.""" if not self.pool: self.fillPool() while 1: self.pool.stir() randomkey = self.pool.get_bytes(40) # First 256 bits are for the session key sessionkey = randomkey[:32] # Last 64 bits are for the CBC initial value. initialvalue = randomkey[-8:] try: self.sym.encryptInit(sessionkey, initialvalue) except TypeError: # Need a new session key (null chars in our key) continue else: break key = "-----BEGIN 411 SHARED KEY-----\n" key += base64.encodestring(randomkey) key += "-----END 411 SHARED KEY-----\n" return key def readSharedKey(self, key64): """Read a 411 shared key in base64 encoding and return the binary bits""" header = "-----BEGIN 411 SHARED KEY-----\n" footer = "-----END 411 SHARED KEY-----\n" try: a = key64.index(header) + len(header) b = key64.index(footer) key = base64.decodestring(key64[a:b]) except: raise Error411, \ "This does not appear to be a 411 shared key." return key def encrypt(self, plaintext, header="-----BEGIN 411 MESSAGE-----\n", footer="-----END 411 MESSAGE-----\n", sign=1): """Encrypts the plain text message using a hybrid cryptography technique: a 256-bit random session key is encrypted with the cluster shared key. The session key is used to quickly encrypt the message with the Blowfish symmetrical algorithm.""" if not self.shared: self.readKeys() # First 256 bits are for the session key, # Last 64 bits are for the CBC initial value. try: self.sym.encryptInit(self.shared[:32], self.shared[-8:]) except TypeError: raise Error411, "Invalid Shared Key" # Sign the text with the master private key if sign: if not self.priv: raise Error411, "I need the master private key to sign messages" sig = self.sign(plaintext) else: sig = "Not Signed" # Encrypt the text with Blowfish for speed. ciphertext = self.sym.update(plaintext) + self.sym.final() ciphertext_base64 = base64.encodestring(ciphertext) # Message format (v2.0): # digital signature # <blank line> # symmetrically-encrypted message msg = header msg += sig + "\n" msg += ciphertext_base64 msg += footer return msg def decrypt(self, contents, header="-----BEGIN 411 MESSAGE-----\n", footer="-----END 411 MESSAGE-----\n", type411=1): """Uses the shared key to read 411 messages. For 411 type messages, returns the tuple (contents, meta) where meta is a dictionary containing the 411 headers. If not type 411, returns a (plaintext, sig_base64). No verification of signature is performed.""" if not self.sym: self.fillPool() ciphersig_base64 = '' try: a = string.index(contents, header) + len(header) b = string.index(contents, footer) msg = contents[a:b] ciphersig_base64, ciphertext_base64 = msg.split('\n\n') ciphertext = base64.decodestring(ciphertext_base64) except: raise Error411, \ "This file does not appear to be in 411 format." if not self.shared: self.readKeys() sessionkey = self.shared[:32] initialvalue = self.shared[-8:] self.sym.decryptInit(sessionkey, initialvalue) try: text = self.sym.update(ciphertext) + self.sym.final() except POW.SSLError: raise Error411, "Could not decrypt file, wrong key?" if type411: if not self.verify(text, ciphersig_base64): raise Error411, "Signature does not verify." return self.decode(text) else: return (text, ciphersig_base64) def decode(self, plaintext): meta = {} p = Parser(plaintext, self.attrs) meta = p.get_filtered_content() self.plugin = p.get_plugin() return meta['content'], meta def verify(self, msg, sig_base64): """Verifies that the plaintext message was signed with the (base64 encoded) signature, and has not been altered since signing. Returns true if message verifies.""" if not self.pub: try: self.readKeys() except IOError, e: syslog.syslog(syslog.LOG_ERR, '411-error: ' \ + str(e)) return 0 digest = POW.Digest(POW.MD5_DIGEST) digest.update(msg) try: sig = base64.decodestring(sig_base64) except: return 0 return self.pub.verify(sig, digest.digest(), POW.MD5_DIGEST)
def bind(self, uuid, alter=0, bogus_binds=0): bind = MSRPCBind(endianness=self.endianness) syntax = '\x04\x5d\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00\x2b\x10\x48\x60' if self.endianness == '>': syntax = unpack('<LHHBB6s', syntax) syntax = pack('>LHHBB6s', *syntax) uuid = list(unpack('<LHHBB6sHH', uuid)) uuid[-1] ^= uuid[-2] uuid[-2] ^= uuid[-1] uuid[-1] ^= uuid[-2] uuid = pack('>LHHBB6sHH', *uuid) ctx = 0 for i in range(bogus_binds): bind.set_ctx_id(self._ctx, index=ctx) bind.set_trans_num(1, index=ctx) bind.set_if_binuuid('A' * 20, index=ctx) bind.set_xfer_syntax_binuuid(syntax, index=ctx) bind.set_xfer_syntax_ver(2, index=ctx) self._ctx += 1 ctx += 1 bind.set_ctx_id(self._ctx, index=ctx) bind.set_trans_num(1, index=ctx) bind.set_if_binuuid(uuid, index=ctx) bind.set_xfer_syntax_binuuid(syntax, index=ctx) bind.set_xfer_syntax_ver(2, index=ctx) bind.set_ctx_num(ctx + 1) if alter: bind.set_type(MSRPC_ALTERCTX) if (self.__auth_level != ntlm.NTLM_AUTH_NONE): if (self.__username is None) or (self.__password is None): self.__username, self.__password, nth, lmh = self._transport.get_credentials( ) auth = ntlm.NTLMAuthNegotiate() auth['auth_level'] = self.__auth_level auth['auth_ctx_id'] = self._ctx + 79231 bind.set_auth_data(str(auth)) self._transport.send(bind.get_packet()) s = self._transport.recv() if s != 0: resp = MSRPCBindAck(s) else: return 0 #mmm why not None? if resp.get_type() == MSRPC_BINDNAK: resp = MSRPCBindNak(s) status_code = resp.get_reason() if rpc_status_codes.has_key(status_code): raise Exception(rpc_status_codes[status_code], resp) else: raise Exception( 'Unknown DCE RPC fault status code: %.8x' % status_code, resp) self.__max_xmit_size = resp.get_max_tfrag() if self.__auth_level != ntlm.NTLM_AUTH_NONE: authResp = ntlm.NTLMAuthChallenge( data=resp.get_auth_data().tostring()) self._ntlm_challenge = authResp['challenge'] response = ntlm.NTLMAuthChallengeResponse(self.__username, self.__password, self._ntlm_challenge) response['auth_ctx_id'] = self._ctx + 79231 response['auth_level'] = self.__auth_level if self.__auth_level in (ntlm.NTLM_AUTH_CONNECT, ntlm.NTLM_AUTH_PKT_INTEGRITY, ntlm.NTLM_AUTH_PKT_PRIVACY): if self.__password: key = ntlm.compute_nthash(self.__password) if POW: hash = POW.Digest(POW.MD4_DIGEST) else: hash = MD4.new() hash.update(key) key = hash.digest() else: key = '\x00' * 16 if POW: cipher = POW.Symmetric(POW.RC4) cipher.encryptInit(key) self.cipher_encrypt = cipher.update else: cipher = ARC4.new(key) self.cipher_encrypt = cipher.encrypt if response['flags'] & ntlm.NTLMSSP_KEY_EXCHANGE: session_key = 'A' * 16 # XXX Generate random session key response['session_key'] = self.cipher_encrypt(session_key) if POW: cipher = POW.Symmetric(POW.RC4) cipher.encryptInit(session_key) self.cipher_encrypt = cipher.update else: cipher = ARC4.new(session_key) self.cipher_encrypt = cipher.encrypt self.sequence = 0 auth3 = MSRPCHeader() auth3.set_type(MSRPC_AUTH3) auth3.set_auth_data(str(response)) self._transport.send(auth3.get_packet(), forceWriteAndx=1) return resp # means packet is signed, if verifier is wrong it fails