summaryrefslogtreecommitdiff
path: root/passlib/handlers/oracle.py
blob: 08850f0c60546582f4fb0e88251f5d722d5483d4 (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
"""passlib.handlers.oracle - Oracle DB Password Hashes"""
#=========================================================
#imports
#=========================================================
#core
from binascii import hexlify, unhexlify
from hashlib import sha1
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
#pkg
from passlib.utils import to_unicode, to_native_str, xor_bytes
from passlib.utils.compat import b, bytes, bascii_to_str, irange, u, \
                                 uascii_to_str, unicode, str_to_uascii
from passlib.utils.des import des_encrypt_block
import passlib.utils.handlers as uh
#local
__all__ = [
    "oracle10g",
    "oracle11g"
]

#=========================================================
#oracle10
#=========================================================
def des_cbc_encrypt(key, value, iv=b('\x00') * 8, pad=b('\x00')):
    """performs des-cbc encryption, returns only last block.

    this performs a specific DES-CBC encryption implementation
    as needed by the Oracle10 hash. it probably won't be useful for
    other purposes as-is.

    input value is null-padded to multiple of 8 bytes.

    :arg key: des key as bytes
    :arg value: value to encrypt, as bytes.
    :param iv: optional IV
    :param pad: optional pad byte

    :returns: last block of DES-CBC encryption of all ``value``'s byte blocks.
    """
    value += pad * (-len(value) % 8) #null pad to multiple of 8
    hash = iv #start things off
    for offset in irange(0,len(value),8):
        chunk = xor_bytes(hash, value[offset:offset+8])
        hash = des_encrypt_block(key, chunk)
    return hash

#: magic string used as initial des key by oracle10
ORACLE10_MAGIC = b("\x01\x23\x45\x67\x89\xAB\xCD\xEF")

class oracle10(uh.HasUserContext, uh.StaticHandler):
    """This class implements the password hash used by Oracle up to version 10g, and follows the :ref:`password-hash-api`.

    It does a single round of hashing, and relies on the username as the salt.

    The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
    following additional contextual keywords:

    :type user: str
    :param user: name of oracle user account this password is associated with.
    """
    #=========================================================
    # algorithm information
    #=========================================================
    name = "oracle10"
    checksum_chars = uh.HEX_CHARS
    checksum_size = 16

    #=========================================================
    # methods
    #=========================================================
    @classmethod
    def _norm_hash(cls, hash):
        return hash.upper()

    def _calc_checksum(self, secret):
        #FIXME: not sure how oracle handles unicode.
        # online docs about 10g hash indicate it puts ascii chars
        # in a 2-byte encoding w/ the high byte set to null.
        # they don't say how it handles other chars, or what encoding.
        #
        # so for now, encoding secret & user to utf-16-be, since that fits,
        # and if secret/user is bytes, we assume utf-8, and decode first.
        #
        # this whole mess really needs someone w/ an oracle system,
        # and some answers :)
        if isinstance(secret, bytes):
            secret = secret.decode("utf-8")
        user = to_unicode(self.user, "utf-8", param="user")
        input = (user+secret).upper().encode("utf-16-be")
        hash = des_cbc_encrypt(ORACLE10_MAGIC, input)
        hash = des_cbc_encrypt(hash, input)
        return hexlify(hash).decode("ascii").upper()

    #=========================================================
    #eoc
    #=========================================================

#=========================================================
#oracle11
#=========================================================
class oracle11(uh.HasSalt, uh.GenericHandler):
    """This class implements the Oracle11g password hash, and follows the :ref:`password-hash-api`.

    It supports a fixed-length salt.

    The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:

    :type salt: str
    :param salt:
        Optional salt string.
        If not specified, one will be autogenerated (this is recommended).
        If specified, it must be 20 hexidecimal characters.
    """
    #=========================================================
    #class attrs
    #=========================================================
    #--GenericHandler--
    name = "oracle11"
    setting_kwds = ("salt",)
    checksum_size = 40
    checksum_chars = uh.UPPER_HEX_CHARS

    _stub_checksum = u('0') * 40

    #--HasSalt--
    min_salt_size = max_salt_size = 20
    salt_chars = uh.UPPER_HEX_CHARS


    #=========================================================
    #methods
    #=========================================================
    _hash_regex = re.compile(u("^S:(?P<chk>[0-9a-f]{40})(?P<salt>[0-9a-f]{20})$"), re.I)

    @classmethod
    def from_string(cls, hash):
        hash = to_unicode(hash, "ascii", "hash")
        m = cls._hash_regex.match(hash)
        if not m:
            raise uh.exc.InvalidHashError(cls)
        salt, chk = m.group("salt", "chk")
        return cls(salt=salt, checksum=chk.upper())

    def to_string(self):
        chk = (self.checksum or self._stub_checksum)
        hash = u("S:%s%s") % (chk.upper(), self.salt.upper())
        return uascii_to_str(hash)

    def _calc_checksum(self, secret):
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")
        chk = sha1(secret + unhexlify(self.salt.encode("ascii"))).hexdigest()
        return str_to_uascii(chk).upper()

    #=========================================================
    #eoc
    #=========================================================

#=========================================================
#eof
#=========================================================