def getrandstr(rng, charset, count): """return string containing *count* number of chars/bytes, whose elements are drawn from specified charset, using specified rng""" # NOTE: tests determined this is 4x faster than rng.sample(), # which is why that's not being used here. # check alphabet & count if count < 0: raise ValueError("count must be >= 0") letters = len(charset) if letters == 0: raise ValueError("alphabet must not be empty") if letters == 1: return charset * count # get random value, and write out to buffer def helper(): # XXX: break into chunks for large number of letters? value = rng.randrange(0, letters**count) i = 0 while i < count: yield charset[value % letters] value //= letters i += 1 if isinstance(charset, unicode): return join_unicode(helper()) else: return join_byte_elems(helper())
def _calc_checksum(self, secret): if isinstance(secret, unicode): # XXX: no idea what unicode policy is, but all examples are # 7-bit ascii compatible, so using UTF-8 secret = secret.encode("utf-8") user = self.user if user: # not positive about this, but it looks like per-user # accounts use the first 4 chars of the username as the salt, # whereas global "enable" passwords don't have any salt at all. if isinstance(user, unicode): user = user.encode("utf-8") secret += user[:4] # null-pad or truncate to 16 bytes secret = right_pad_string(secret, 16) # md5 digest hash = md5(secret).digest() # drop every 4th byte hash = join_byte_elems(c for i, c in enumerate(hash) if i & 3 < 3) # encode using Hash64 return h64.encode_bytes(hash).decode("ascii")
def _calc_checksum(self, secret): if isinstance(secret, unicode): # XXX: no idea what unicode policy is, but all examples are # 7-bit ascii compatible, so using UTF-8 secret = secret.encode("utf-8") user = self.user if user: # NOTE: not *positive* about this, but it looks like per-user # accounts use first 4 chars of user as salt, whereas global # "enable" passwords don't have any salt at all. if isinstance(user, unicode): user = user.encode("utf-8") secret += user[:4] # pad/truncate to 16 secret = right_pad_string(secret, 16) # md5 digest hash = md5(secret).digest() # drop every 4th byte hash = join_byte_elems(c for i,c in enumerate(hash) if i & 3 < 3) # encode using Hash64 return h64.encode_bytes(hash).decode("ascii")
def encode_int12(self, value): """encodes 12-bit integer -> 2 char string""" if value < 0 or value > 0xFFF: raise ValueError("value out of range") raw = [value & 0x3f, (value>>6) & 0x3f] if self.big: raw = reversed(raw) return join_byte_elems(imap(self._encode64, raw))
def encode_int12(self, value): """encodes 12-bit integer -> 2 char string""" if value < 0 or value > 0xFFF: raise ValueError("value out of range") raw = [value & 0x3f, (value >> 6) & 0x3f] if self.big: raw = reversed(raw) return join_byte_elems(imap(self._encode64, raw))
def decode_transposed_bytes(self, source, offsets): """decode byte string, then reverse transposition described by offset list""" # NOTE: if transposition does not use all bytes of source, # the original can't be recovered... and join_byte_elems() will throw # an error because 1+ values in <buf> will be None. tmp = self.decode_bytes(source) buf = [None] * len(offsets) for off, char in zip(offsets, tmp): buf[off] = char return join_byte_elems(buf)
def _calc_checksum(self, secret): # This function handles both the cisco_pix & cisco_asa formats: # * PIX had a limit of 16 character passwords, and always appended the username. # * ASA 7.0 (2005) increases this limit to 32, and conditionally appends the username. # The two behaviors are controlled based on the _is_asa class-level flag. asa = self._is_asa # XXX: No idea what unicode policy is, but all examples are # 7-bit ascii compatible, so using UTF-8. if isinstance(secret, unicode): secret = secret.encode("utf-8") seclen = len(secret) # check for truncation (during .hash() calls only) if self.use_defaults: self._check_truncate_policy(secret) # PIX/ASA: Per-user accounts use the first 4 chars of the username as the salt, # whereas global "enable" passwords don't have any salt at all. # ASA only: Don't append user if password is 28 or more characters. user = self.user if user and not (asa and seclen > 27): if isinstance(user, unicode): user = user.encode("utf-8") secret += user[:4] # PIX: null-pad or truncate to 16 bytes. # ASA: increase to 32 bytes if password is 13 or more characters. if asa and seclen > 12: padsize = 32 else: padsize = 16 secret = right_pad_string(secret, padsize) # md5 digest hash = md5(secret).digest() # drop every 4th byte hash = join_byte_elems(c for i,c in enumerate(hash) if i & 3 < 3) # encode using Hash64 return h64.encode_bytes(hash).decode("ascii")
def encode_bytes(self, source): """encode bytes to base64 string. :arg source: byte string to encode. :returns: byte string containing encoded data. """ if not isinstance(source, bytes): raise TypeError("source must be bytes, not %s" % (type(source), )) chunks, tail = divmod(len(source), 3) if PY3: next_value = nextgetter(iter(source)) else: next_value = nextgetter(ord(elem) for elem in source) gen = self._encode_bytes(next_value, chunks, tail) out = join_byte_elems(imap(self._encode64, gen)) # if tail: ## padding = self.padding # if padding: ## out += padding * (3-tail) return out
def encode_bytes(self, source): """encode bytes to base64 string. :arg source: byte string to encode. :returns: byte string containing encoded data. """ if not isinstance(source, bytes): raise TypeError("source must be bytes, not %s" % (type(source),)) chunks, tail = divmod(len(source), 3) if PY3: next_value = nextgetter(iter(source)) else: next_value = nextgetter(ord(elem) for elem in source) gen = self._encode_bytes(next_value, chunks, tail) out = join_byte_elems(imap(self._encode64, gen)) ##if tail: ## padding = self.padding ## if padding: ## out += padding * (3-tail) return out
def _encode_int(self, value, bits): """encode integer into base64 format :arg value: non-negative integer to encode :arg bits: number of bits to encode :returns: a string of length ``int(ceil(bits/6.0))``. """ assert value >= 0, "caller did not sanitize input" pad = -bits % 6 bits += pad if self.big: itr = irange(bits-6, -6, -6) # shift to add lsb padding. value <<= pad else: itr = irange(0, bits, 6) # padding is msb, so no change needed. return join_byte_elems(imap(self._encode64, ((value>>off) & 0x3f for off in itr)))
def _encode_int(self, value, bits): """encode integer into base64 format :arg value: non-negative integer to encode :arg bits: number of bits to encode :returns: a string of length ``int(ceil(bits/6.0))``. """ assert value >= 0, "caller did not sanitize input" pad = -bits % 6 bits += pad if self.big: itr = irange(bits - 6, -6, -6) # shift to add lsb padding. value <<= pad else: itr = irange(0, bits, 6) # padding is msb, so no change needed. return join_byte_elems( imap(self._encode64, ((value >> off) & 0x3f for off in itr)))
def encode_transposed_bytes(self, source, offsets): """encode byte string, first transposing source using offset list""" if not isinstance(source, bytes): raise TypeError("source must be bytes, not %s" % (type(source),)) tmp = join_byte_elems(source[off] for off in offsets) return self.encode_bytes(tmp)
def encode_transposed_bytes(self, source, offsets): """encode byte string, first transposing source using offset list""" if not isinstance(source, bytes): raise TypeError("source must be bytes, not %s" % (type(source), )) tmp = join_byte_elems(source[off] for off in offsets) return self.encode_bytes(tmp)
def _calc_checksum(self, secret): """ This function implements the "encrypted" hash format used by Cisco PIX & ASA. It's behavior has been confirmed for ASA 9.6, but is presumed correct for PIX & other ASA releases, as it fits with known test vectors, and existing literature. While nearly the same, the PIX & ASA hashes have slight differences, so this function performs differently based on the _is_asa class flag. Noteable changes from PIX to ASA include password size limit increased from 16 -> 32, and other internal changes. """ # select PIX vs or ASA mode asa = self._is_asa # # encode secret # # per ASA 8.4 documentation, # http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets, # it supposedly uses UTF-8 -- though some double-encoding issues have # been observed when trying to actually *set* a non-ascii password # via ASDM, and access via SSH seems to strip 8-bit chars. # if isinstance(secret, unicode): secret = secret.encode("utf-8") # # check if password too large # # Per ASA 9.6 changes listed in # http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html, # prior releases had a maximum limit of 32 characters. # Testing with an ASA 9.6 system bears this out -- # setting 32-char password for a user account, # and logins will fail if any chars are appended. # (ASA 9.6 added new PBKDF2-based hash algorithm, # which supports larger passwords). # # Per PIX documentation # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html, # it would not allow passwords > 16 chars. # # Thus, we unconditionally throw a password size error here, # as nothing valid can come from a larger password. # NOTE: assuming PIX has same behavior, but at 16 char limit. # spoil_digest = None if len(secret) > self.truncate_size: if self.use_defaults: # called from hash() msg = "Password too long (%s allows at most %d bytes)" % ( self.name, self.truncate_size, ) raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg) else: # called from verify() -- # We don't want to throw error, or return early, # as that would let attacker know too much. Instead, we set a # flag to add some dummy data into the md5 digest, so that # output won't match truncated version of secret, or anything # else that's fixed and predictable. spoil_digest = secret + _DUMMY_BYTES # # append user to secret # # Policy appears to be: # # * Nothing appended for enable password (user = "") # # * ASA: If user present, but secret is >= 28 chars, nothing appended. # # * 1-2 byte users not allowed. # DEVIATION: we're letting them through, and repeating their # chars ala 3-char user, to simplify testing. # Could issue warning in the future though. # # * 3 byte user has first char repeated, to pad to 4. # (observed under ASA 9.6, assuming true elsewhere) # # * 4 byte users are used directly. # # * 5+ byte users are truncated to 4 bytes. # user = self.user if user: if isinstance(user, unicode): user = user.encode("utf-8") if not asa or len(secret) < 28: secret += repeat_string(user, 4) # # pad / truncate result to limit # # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF # secret+user > 16 bytes. This makes PIX & ASA have different results # where secret size in range(13,16), and user is present -- # PIX will truncate to 16, ASA will truncate to 32. # if asa and len(secret) > 16: pad_size = 32 else: pad_size = 16 secret = right_pad_string(secret, pad_size) # # md5 digest # if spoil_digest: # make sure digest won't match truncated version of secret secret += spoil_digest digest = md5(secret).digest() # # drop every 4th byte # NOTE: guessing this was done because it makes output exactly # 16 bytes, which may have been a general 'char password[]' # size limit under PIX # digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3) # # encode using Hash64 # return h64.encode_bytes(digest).decode("ascii")
def _calc_checksum(self, secret): """ This function implements the "encrypted" hash format used by Cisco PIX & ASA. It's behavior has been confirmed for ASA 9.6, but is presumed correct for PIX & other ASA releases, as it fits with known test vectors, and existing literature. While nearly the same, the PIX & ASA hashes have slight differences, so this function performs differently based on the _is_asa class flag. Noteable changes from PIX to ASA include password size limit increased from 16 -> 32, and other internal changes. """ # select PIX vs or ASA mode asa = self._is_asa # # encode secret # # per ASA 8.4 documentation, # http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets, # it supposedly uses UTF-8 -- though some double-encoding issues have # been observed when trying to actually *set* a non-ascii password # via ASDM, and access via SSH seems to strip 8-bit chars. # if isinstance(secret, unicode): secret = secret.encode("utf-8") # # check if password too large # # Per ASA 9.6 changes listed in # http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html, # prior releases had a maximum limit of 32 characters. # Testing with an ASA 9.6 system bears this out -- # setting 32-char password for a user account, # and logins will fail if any chars are appended. # (ASA 9.6 added new PBKDF2-based hash algorithm, # which supports larger passwords). # # Per PIX documentation # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html, # it would not allow passwords > 16 chars. # # Thus, we unconditionally throw a password size error here, # as nothing valid can come from a larger password. # NOTE: assuming PIX has same behavior, but at 16 char limit. # spoil_digest = None if len(secret) > self.truncate_size: if self.use_defaults: # called from hash() msg = "Password too long (%s allows at most %d bytes)" % \ (self.name, self.truncate_size) raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg) else: # called from verify() -- # We don't want to throw error, or return early, # as that would let attacker know too much. Instead, we set a # flag to add some dummy data into the md5 digest, so that # output won't match truncated version of secret, or anything # else that's fixed and predictable. spoil_digest = secret + _DUMMY_BYTES # # append user to secret # # Policy appears to be: # # * Nothing appended for enable password (user = "") # # * ASA: If user present, but secret is >= 28 chars, nothing appended. # # * 1-2 byte users not allowed. # DEVIATION: we're letting them through, and repeating their # chars ala 3-char user, to simplify testing. # Could issue warning in the future though. # # * 3 byte user has first char repeated, to pad to 4. # (observed under ASA 9.6, assuming true elsewhere) # # * 4 byte users are used directly. # # * 5+ byte users are truncated to 4 bytes. # user = self.user if user: if isinstance(user, unicode): user = user.encode("utf-8") if not asa or len(secret) < 28: secret += repeat_string(user, 4) # # pad / truncate result to limit # # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF # secret+user > 16 bytes. This makes PIX & ASA have different results # where secret size in range(13,16), and user is present -- # PIX will truncate to 16, ASA will truncate to 32. # if asa and len(secret) > 16: pad_size = 32 else: pad_size = 16 secret = right_pad_string(secret, pad_size) # # md5 digest # if spoil_digest: # make sure digest won't match truncated version of secret secret += spoil_digest digest = md5(secret).digest() # # drop every 4th byte # NOTE: guessing this was done because it makes output exactly # 16 bytes, which may have been a general 'char password[]' # size limit under PIX # digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3) # # encode using Hash64 # return h64.encode_bytes(digest).decode("ascii")