diff --git a/README.md b/README.md
index 47f5fcb..25f9da7 100644
--- a/README.md
+++ b/README.md
@@ -267,6 +267,9 @@ python setup.py tests
 
 ### Changelog
 
+##### 0.3.21
++ Fix bug with scoped key generation not working with newer Keen projects.
+
 ##### 0.3.20
 + Add `saved_queries` support
 + Add Python 3.4 support
diff --git a/keen/Padding.py b/keen/Padding.py
index 5615340..b213370 100644
--- a/keen/Padding.py
+++ b/keen/Padding.py
@@ -103,7 +103,10 @@ def appendCMSPadding(str, blocksize=AES_blocksize):
 
 def removeCMSPadding(str, blocksize=AES_blocksize):
   '''CMS padding: Remove padding with bytes containing the number of padding bytes '''
-  pad_len = ord(str[-1]) # last byte contains number of padding bytes
+  try:
+    pad_len = ord(str[-1]) # last byte contains number of padding bytes
+  except TypeError:
+    pad_len = str[-1]
   assert pad_len <= blocksize, 'padding error' 
   assert pad_len <= len(str), 'padding error'
     
@@ -152,7 +155,10 @@ def appendZeroLenPadding(str, blocksize=AES_blocksize):
 
 def removeZeroLenPadding(str, blocksize=AES_blocksize):
   'Remove Padding with zeroes + last byte equal to the number of padding bytes'
-  pad_len = ord(str[-1]) # last byte contains number of padding bytes
+  try:
+    pad_len = ord(str[-1]) # last byte contains number of padding bytes
+  except TypeError:
+    pad_len = str[-1]
   assert pad_len < blocksize, 'padding error' 
   assert pad_len < len(str), 'padding error'
     
diff --git a/keen/scoped_keys.py b/keen/scoped_keys.py
index 4697c3b..e064c9b 100644
--- a/keen/scoped_keys.py
+++ b/keen/scoped_keys.py
@@ -8,22 +8,22 @@ from keen import Padding
 __author__ = 'dkador'
 
 # the block size for the cipher object; must be 16, 24, or 32 for AES
-BLOCK_SIZE = 32
+OLD_BLOCK_SIZE = 32
 
 
-def _pad(s):
+def pad_aes256(s):
     """
     Pads an input string to a given block size.
     :param s: string
     :returns: The padded string.
     """
-    if len(s) % BLOCK_SIZE == 0:
+    if len(s) % AES.block_size == 0:
         return s
 
-    return Padding.appendPadding(s, blocksize=BLOCK_SIZE)
+    return Padding.appendPadding(s, blocksize=AES.block_size)
 
 
-def _unpad(s):
+def unpad_aes256(s):
     """
     Removes padding from an input string based on a given block size.
     :param s: string
@@ -33,14 +33,96 @@ def _unpad(s):
         return s
 
     try:
-        return Padding.removePadding(s, blocksize=BLOCK_SIZE)
+        return Padding.removePadding(s, blocksize=AES.block_size)
     except AssertionError:
         # if there's an error while removing padding, just return s.
         return s
 
 
-# encrypt with AES, encode with hex
-def _encode_aes(key, plaintext):
+def old_pad(s):
+    """
+    Pads an input string to a given block size.
+    :param s: string
+    :returns: The padded string.
+    """
+    if len(s) % OLD_BLOCK_SIZE == 0:
+        return s
+
+    return Padding.appendPadding(s, blocksize=OLD_BLOCK_SIZE)
+
+
+def old_unpad(s):
+    """
+    Removes padding from an input string based on a given block size.
+    :param s: string
+    :returns: The unpadded string.
+    """
+    if not s:
+        return s
+
+    try:
+        return Padding.removePadding(s, blocksize=OLD_BLOCK_SIZE)
+    except AssertionError:
+        # if there's an error while removing padding, just return s.
+        return s
+
+
+# encrypt with AES-256-CBC, encode with hex
+def encode_aes256(key, plaintext):
+    """
+    Utility method to encode some given plaintext with the given key. Important thing to note:
+
+    This is not a general purpose encryption method - it has specific semantics (see below for
+    details).
+
+    Takes the given hex string key and converts it to a 256 bit binary blob. Then pads the given
+    plaintext to AES block size which is always 16 bytes, regardless of AES key size. Then
+    encrypts using AES-256-CBC using a random IV. Then converts both the IV and the ciphertext
+    to hex. Finally returns the IV appended by the ciphertext.
+
+    :param key: string, 64 hex chars long
+    :param plaintext: string, any amount of data
+    """
+    if len(key) != 64:
+        raise TypeError("encode_aes256() expects a 256 bit key encoded as a 64 hex character string")
+
+    # generate AES.block_size cryptographically secure random bytes for our IV (initial value)
+    iv = os.urandom(AES.block_size)
+    # set up an AES cipher object
+    cipher = AES.new(binascii.unhexlify(key.encode('ascii')), mode=AES.MODE_CBC, IV=iv)
+    # encrypt the plaintext after padding it
+    ciphertext = cipher.encrypt(pad_aes256(plaintext))
+    # append the hexed IV and the hexed ciphertext
+    iv_plus_encrypted = binascii.hexlify(iv) + binascii.hexlify(ciphertext)
+    # return that
+    return iv_plus_encrypted
+
+
+def decode_aes256(key, iv_plus_encrypted):
+    """
+    Utility method to decode a payload consisting of the hexed IV + the hexed ciphertext using
+    the given key. See above for more details.
+
+    :param key: string, 64 hex characters long
+    :param iv_plus_encrypted: string, a hexed IV + hexed ciphertext
+    """
+    # grab first AES.block_size bytes (aka 2 * AES.block_size characters of hex) - that's the IV
+    iv_size = 2 * AES.block_size
+    hexed_iv = iv_plus_encrypted[:iv_size]
+    # grab everything else - that's the ciphertext (aka encrypted message)
+    hexed_ciphertext = iv_plus_encrypted[iv_size:]
+    # unhex the iv and ciphertext
+    iv = binascii.unhexlify(hexed_iv)
+    ciphertext = binascii.unhexlify(hexed_ciphertext)
+    # set up the correct AES cipher object
+    cipher = AES.new(binascii.unhexlify(key.encode('ascii')), mode=AES.MODE_CBC, IV=iv)
+    # decrypt!
+    plaintext = cipher.decrypt(ciphertext)
+    # return the unpadded version of this
+    return unpad_aes256(plaintext)
+
+
+def old_encode_aes(key, plaintext):
     """
     Utility method to encode some given plaintext with the given key. Important thing to note:
 
@@ -57,16 +139,16 @@ def _encode_aes(key, plaintext):
     # generate 16 cryptographically secure random bytes for our IV (initial value)
     iv = os.urandom(16)
     # set up an AES cipher object
-    cipher = AES.new(_pad(key), mode=AES.MODE_CBC, IV=iv)
+    cipher = AES.new(old_pad(key), mode=AES.MODE_CBC, IV=iv)
     # encrypte the plaintext after padding it
-    ciphertext = cipher.encrypt(_pad(plaintext))
+    ciphertext = cipher.encrypt(old_pad(plaintext))
     # append the hexed IV and the hexed ciphertext
     iv_plus_encrypted = binascii.hexlify(iv) + binascii.hexlify(ciphertext)
     # return that
     return iv_plus_encrypted
 
 
-def _decode_aes(key, iv_plus_encrypted):
+def old_decode_aes(key, iv_plus_encrypted):
     """
     Utility method to decode a payload consisting of the hexed IV + the hexed ciphertext using
     the given key. See above for more details.
@@ -82,18 +164,27 @@ def _decode_aes(key, iv_plus_encrypted):
     iv = binascii.unhexlify(hexed_iv)
     ciphertext = binascii.unhexlify(hexed_ciphertext)
     # set up the correct AES cipher object
-    cipher = AES.new(_pad(key), mode=AES.MODE_CBC, IV=iv)
+    cipher = AES.new(old_pad(key), mode=AES.MODE_CBC, IV=iv)
     # decrypt!
     plaintext = cipher.decrypt(ciphertext)
     # return the unpadded version of this
-    return _unpad(plaintext)
+    return old_unpad(plaintext)
 
 
 def encrypt(api_key, options):
     options_string = json.dumps(options)
-    return _encode_aes(api_key, options_string)
+    if len(api_key) == 64:
+        return encode_aes256(api_key, options_string)
+    else:
+        return old_encode_aes(api_key, options_string)
 
 
 def decrypt(api_key, scoped_key):
-    json_string = _decode_aes(api_key, scoped_key)
-    return json.loads(json_string)
\ No newline at end of file
+    if len(api_key) == 64:
+        json_string = decode_aes256(api_key, scoped_key)
+    else:
+        json_string = old_decode_aes(api_key, scoped_key)
+    try:
+        return json.loads(json_string)
+    except TypeError:
+        return json.loads(json_string.decode())
diff --git a/setup.py b/setup.py
index 1aa6a03..79eeb52 100644
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ if sys.version_info < (2, 7):
 
 setup(
     name="keen",
-    version="0.3.20",
+    version="0.3.21",
     description="Python Client for Keen IO",
     author="Keen IO",
     author_email="team@keen.io",

New projects have 64-byte master keys, pycrypto supports only 16/24/32-byte AES
Generating scoped keys is breaking on newer projects, as the master keys are now 64-byte. They seemed to have changed from 32-byte keys in the past few months. I created a project a month or two ago that has a 32-byte key, and year+ old projects have 32-byte keys. Only the newer one from a week or two ago has 64-byte.

``` python
scoped_key = scoped_keys.encrypt(master_key, {
  "allowed_operations": ["read"],
  "filters": filters
})
```

Exception being thrown:
`ValueError: AES key must be either 16, 24, or 32 bytes long` from pycrypto.

My `keen` module is the lastest `0.3.20`, and `pycrypto` is the latest `2.6.1`.
diff --git a/keen/tests/scoped_key_tests.py b/keen/tests/scoped_key_tests.py
new file mode 100644
index 0000000..ff65c31
--- /dev/null
+++ b/keen/tests/scoped_key_tests.py
@@ -0,0 +1,42 @@
+from keen import scoped_keys
+from keen.tests.base_test_case import BaseTestCase
+
+
+class ScopedKeyTests(BaseTestCase):
+    api_key = "24077ACBCB198BAAA2110EDDB673282F8E34909FD823A15C55A6253A664BE368"
+    bad_api_key = "24077ACBCB198BAAA2110EDDB673282F8E34909FD823A15C55A6253A664BE369"
+    old_api_key = "ab428324dbdbcfe744"
+    old_bad_api_key = "badbadbadbad"
+    options = {
+        "filters": [{
+            "property_name": "accountId",
+            "operator": "eq",
+            "property_value": "123456"
+        }]
+    }
+
+    def test_scoped_key_encrypts_and_decrypts(self):
+        encrypted = scoped_keys.encrypt(self.api_key, self.options)
+        decrypted = scoped_keys.decrypt(self.api_key, encrypted)
+        self.assert_equal(decrypted, self.options)
+
+    def test_scoped_key_fails_decryption_bad_key(self):
+        encrypted = scoped_keys.encrypt(self.api_key, self.options)
+        try:
+            scoped_keys.decrypt(self.bad_api_key, encrypted)
+            self.fail("shouldn't get here")
+        except ValueError as e:
+            self.assert_not_equal(e, None)
+
+    def test_old_scoped_key_encrypts_and_decrypts(self):
+        encrypted = scoped_keys.encrypt(self.old_api_key, self.options)
+        decrypted = scoped_keys.decrypt(self.old_api_key, encrypted)
+        self.assert_equal(decrypted, self.options)
+
+    def test_old_scoped_key_fails_decryption_on_bad_key(self):
+        encrypted = scoped_keys.encrypt(self.old_api_key, self.options)
+        try:
+            scoped_keys.decrypt(self.old_bad_api_key, encrypted)
+            self.fail("shouldn't get here")
+        except ValueError as e:
+            self.assert_not_equal(e, None)

pip install -e .
parse_log_pytest
requirements.txt
nose mock responses unittest2 pytest
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
exceptiongroup==1.2.2
idna==3.10
iniconfig==2.1.0
-e git+https://github.com/keenlabs/KeenClient-Python.git@add5f7f54acdc32577db690b682c4708a783c735#egg=keen
linecache2==1.0.0
mock==5.2.0
nose==1.3.7
packaging==24.2
Padding==0.5
pluggy==1.5.0
pycrypto==2.6.1
pytest==8.3.5
PyYAML==6.0.2
requests==2.32.3
responses==0.25.7
six==1.17.0
tomli==2.2.1
traceback2==1.4.0
unittest2==1.1.0
urllib3==2.3.0

name: KeenClient-Python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - argparse==1.4.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - exceptiongroup==1.2.2
      - idna==3.10
      - iniconfig==2.1.0
      - linecache2==1.0.0
      - mock==5.2.0
      - nose==1.3.7
      - packaging==24.2
      - padding==0.5
      - pluggy==1.5.0
      - pycrypto==2.6.1
      - pytest==8.3.5
      - pyyaml==6.0.2
      - requests==2.32.3
      - responses==0.25.7
      - six==1.17.0
      - tomli==2.2.1
      - traceback2==1.4.0
      - unittest2==1.1.0
      - urllib3==2.3.0
prefix: /opt/conda/envs/KeenClient-Python

keen/tests/scoped_key_tests.py::ScopedKeyTests::test_old_scoped_key_encrypts_and_decrypts keen/tests/scoped_key_tests.py::ScopedKeyTests::test_old_scoped_key_fails_decryption_on_bad_key keen/tests/scoped_key_tests.py::ScopedKeyTests::test_scoped_key_encrypts_and_decrypts keen/tests/scoped_key_tests.py::ScopedKeyTests::test_scoped_key_fails_decryption_bad_key
setup.py keen/scoped_keys.py README.md keen/Padding.py
setup.py keen/scoped_keys.py README.md keen/Padding.py