javax.crypto.BadPaddingException: Given final block not properly padded - Strange error - java

javax.crypto.BadPaddingException: Given final block not properly padded - This is the error which I've got. I do not know why. My code seems to be alright. The key is the same during encrypting and decrypting. What I can say is that none of the already answered questions contain solution for my problem. Here is my code:
public String decrypt(String text) throws Exception {
String key = "SiadajerSiadajer"; // 128 bit key
// Create key and cipher
Key aesKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES");
// encrypt the text
byte[]encrypted = text.getBytes();
cipher.init(Cipher.DECRYPT_MODE, aesKey);
String decrypted = new String(cipher.doFinal(encrypted)); //Here is the error
return decrypted;
}
And the encryption mechanism in php
function encrypt($data, $size)
{
$length = $size - strlen($data) % $size;
return $data . str_repeat(chr($length), $length);
}
function decrypt($data)
{
return substr($data, 0, -ord($data[strlen($data) - 1]));
}
$key = "SiadajerSiadajer";
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
$name = openssl_encrypt(encrypt($name, 16), 'AES-256-CBC', $key, 0, $iv);
EDIT
Now in the php part my code is:
$key = "SiadajerSiadajer";
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
$EIV = base64_encode($iv);
$name = openssl_encrypt(encrypt($name, 16), 'AES-256-CBC', $key, 0, $EIV);
And it gives me warning:Warning: openssl_encrypt(): IV passed is 24 bytes long which is longer than the 16 expected by selected cipher, truncating in C:\xampp2\htdocs\standardfinalinserting.php on line 68
And in Java part my decryption method is exactly like in the answer for my question but after running it gives me an error: java.security.InvalidKeyException: Illegal key size on the line:
cipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);
EDIT 2:
So this is my whole Main class. It contains your whole Java code example
public class Main {
private byte[] padKey(byte[] key) {
byte[] paddedKey = new byte[32];
System.arraycopy(key, 0, paddedKey, 0, key.length);
return paddedKey;
}
private byte[] unpad(byte[] data) {
byte[] unpaddedData = new byte[data.length - data[data.length - 1]];
System.arraycopy(data, 0, unpaddedData, 0, unpaddedData.length);
return unpaddedData;
}
public String decrypt(String encodedJoinedData) throws Exception {
// Base64-decode the joined data
byte[] joinedData = Base64.decode(encodedJoinedData);
// Get IV and encrypted data
byte[] iv = new byte[16];
System.arraycopy(joinedData, 0, iv, 0, iv.length);
byte[] encryptedData = new byte[joinedData.length - iv.length];
System.arraycopy(joinedData, iv.length, encryptedData, 0, encryptedData.length);
// Pad key
byte[] key = padKey("SiadajerSiadajer".getBytes());
Key aesKey = new SecretKeySpec(key, "AES");
// Specify CBC-mode
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec); //HERE IS THE ERROR
// Decrypt data
byte[] decryptedData = cipher.doFinal(encryptedData);
// Remove custom padding
byte[] unpaddedData = unpad(decryptedData);
return new String(unpaddedData);
}
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
String encodedJoinedData = "RB6Au33KGOF1/z3BQKqievUmh81Y8q4uw7s4vEs9xurQmNZAKwmhRQtS9wGYKQj8cJaPpK2xaDzx3RppQ8PsM/rQ9/p0Lme+x75dozBEbmFn6Q9eCXOCiCivVsKzZ8Vz";
String decryptedData = new Main().decrypt(encodedJoinedData);
System.out.println(decryptedData + " - " + decryptedData.length());
}
}
Running the code results in error:
Exception in thread "main" java.security.InvalidKeyException: Illegal key size
at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1039)
at javax.crypto.Cipher.implInit(Cipher.java:805)
at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
at javax.crypto.Cipher.init(Cipher.java:1396)
at javax.crypto.Cipher.init(Cipher.java:1327)
at com.dd.escuel.Main.decrypt(Main.java:43)
at com.dd.escuel.Main.main(Main.java:57)

There are a few issues with the Java-code:
In the PHP-code AES-256 is used, thus, the key must have a length of 32 Byte. Shorter keys are automatically right padded with zeros. This happens in your PHP-code since your key SiadajerSiadajer has a length of only 16 Byte. The key-padding must also be done in the Java-code. For this, e.g. the following Java-method can be used:
private byte[] padKey(byte[] key) {
byte[] paddedKey = new byte[32];
System.arraycopy(key, 0, paddedKey, 0, key.length);
return paddedKey;
}
With Cipher.getInstance("AES") the ECB-mode and PKCS5Padding are chosen by default. The CBC-mode must be explicitly specified in the Java-code with
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
In the PHP-method openssl_encrypt the fourth parameter $options is set to 0 which means that the returned data are Base64-encoded. Thus, in the Java-code the data have to be Base64-decoded before decryption:
byte[]encryptedData = Base64.decode(text);
Since the CBC-mode is used the IV of the encryption must be considered. A possible approach is to Base64-encode the IV in the PHP-code (subsequent to the encryption) with
$encodedIV = base64_encode($iv);
and pass that value to the Java-method decrypt as second parameter. Here, the IV must be decoded and used for the decryption:
byte[] iv = Base64.decode(ivEncoded);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
...
cipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);
In the PHP-method openssl_encrypt the fourth parameter $options is set to 0 which means that the default padding (PKCS7) is used. Moreover, in the PHP-method encrypt a kind of custom padding is implemented (btw: the name of the method is unsuitable since it doesn't encrypt) and thus, it's padded twice. Therefore, after the decryption the custom padding (which may consist of white spaces) must be removed in the Java-code:
byte[] unpaddedData = unpad(decryptedData);
with
private byte[] unpad(byte[] data) {
byte[] unpaddedData = new byte[data.length - data[data.length - 1]];
System.arraycopy(data, 0, unpaddedData, 0, unpaddedData.length);
return unpaddedData;
}
Altogether:
public String decrypt(String text, String ivEncoded) throws Exception {
// Pad key
byte[] key = padKey("SiadajerSiadajer".getBytes());
Key aesKey = new SecretKeySpec(key, "AES");
// Base64 decode data
byte[]encryptedData = Base64.decode(text);
// Base64 decode iv
byte[] iv = Base64.decode(ivEncoded);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
// Specify CBC-mode
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);
// Decrypt
byte[] decryptedData = cipher.doFinal(encryptedData);
// Remove custom padding
byte[] unpaddedData = unpad(decryptedData);
return new String(unpaddedData);
}
Test:
PHP-code input: Plain text ($data), key ($key):
$data = 'This is a plain text which needs to be encrypted...';
$key = "SiadajerSiadajer";
PHP-code output: Base64 encoded IV ($encodedIV), encrypted data ($name):
$encodedIV = 'Dg+Zs3mqIJeDOOEPMT5F4Q==';
$name = '8dXjeQhx2WSswQOQXLcyMKNpa5s413yI2Ku8WiIB/xtA2pEjrKcl5kWtrOh9k4A12Jl0N/z6tH67Wybhp/OwTi1NtiJOZxl3w6YQufE29oU=';
If that output is used as input for the Java-method decrypt the decrypted data equals the plain text.
Concerning the PHP-code I would suggest to remove either the custom or the default (PKCS7) padding (if you have the choice). The latter can be achieved by using the flag OPENSSL_ZERO_PADDING as fourth parameter in the openssl_encrypt method (note: that flag doesn't mean "pad with zero-values", but "no padding"). If the custom padding is kept, you should at least rename the PHP-methods encrypt and decrypt to pad and unpad (or something similar), respectively.
As stated already in the comments, the GCM-mode may be a better choice than the-CBC mode. However, it would be useful to get informed about the basics before coding, e.g. difference between CBC- and GCM-mode,
explanation of the GCM-mode here and here and the pitfalls coming along with the GCM-mode (GCM is secure, but only if you follow certain guidelines, e.g. a uniqe IV/nonce for each message encrypted with the same key).
You can use the PHP-method openssl_get_cipher_methods to find out the permissible AES-modes supported in PHP. This is explained in more detail here. For AES-256 and the AES-mode GCM you have to specify aes-256-gcm (with lowercase(!) letters). Presumably, that's why you get the "Unknown cipher algorithm"-error.
EDIT:
You can use for the encryption the following PHP-code (which is a slightly modified version of your PHP-code from the question):
<?php
function pad($data, $size) {
$length = $size - strlen($data) % $size;
return $data . str_repeat(chr($length), $length);
}
function unpad($data) {
return substr($data, 0, -ord($data[strlen($data) - 1]));
}
$data = 'This is a plain text which needs to be encrypted...';
$key = "SiadajerSiadajer";
$iv_size = 16;
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
$encryptedData = openssl_encrypt(pad($data, 16), 'AES-256-CBC', $key, 0, $iv);
print base64_encode($iv)."\n".$encryptedData."\n";
EDIT 2:
The IV and the encrypted data can be joined before or after the Base64-encoding. The first is more efficient and thus, I've implemented this variant. However, some changes are needed in the PHP- and Java-code and therefore, I post all methods which have changed.
The PHP-code becomes:
<?php
function pad($data, $size) {
$length = $size - strlen($data) % $size;
return $data . str_repeat(chr($length), $length);
}
function unpad($data) {
return substr($data, 0, -ord($data[strlen($data) - 1]));
}
$data = 'This is a plain text which needs to be encrypted...';
$key = "SiadajerSiadajer";
$iv_size = 16;
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
$encryptedData = openssl_encrypt(pad($data, 16), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
$joinedData = $iv.$encryptedData;
$encodedJoinedData = base64_encode($joinedData);
print $encodedJoinedData."\n";
And the Jave-method decrypt becomes:
public String decrypt(String encodedJoinedData) throws Exception {
// Base64-decode the joined data
byte[] joinedData = Base64.decode(encodedJoinedData);
// Get IV and encrypted data
byte[] iv = new byte[16];
System.arraycopy(joinedData, 0, iv, 0, iv.length);
byte[] encryptedData = new byte[joinedData.length - iv.length];
System.arraycopy(joinedData, iv.length, encryptedData, 0, encryptedData.length);
// Pad key
byte[] key = padKey("SiadajerSiadajer".getBytes());
Key aesKey = new SecretKeySpec(key, "AES");
// Specify CBC-mode
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);
// Decrypt data
byte[] decryptedData = cipher.doFinal(encryptedData);
// Remove custom padding
byte[] unpaddedData = unpad(decryptedData);
return new String(unpaddedData);
}
and an example for the Java-method main is:
public static void main(String[] args) throws Exception {
String encodedJoinedData = "RB6Au33KGOF1/z3BQKqievUmh81Y8q4uw7s4vEs9xurQmNZAKwmhRQtS9wGYKQj8cJaPpK2xaDzx3RppQ8PsM/rQ9/p0Lme+x75dozBEbmFn6Q9eCXOCiCivVsKzZ8Vz";
String decryptedData = new Main().decrypt(encodedJoinedData);
System.out.println(decryptedData + " - " + decryptedData.length());
}
The usage is as follows:
Encrypt your plain text with the PHP-code. In the example above (PHP-code) the plain text is
$data = 'This is a plain text which needs to be encrypted...';
Then, pass the string contained in $encodedJoinedData to the Java-method decrypt. In the example above (main-method) the string is
String encodedJoinedData = "RB6Au33KGOF1/z3BQKqievUmh81Y8q4uw7s4vEs9xurQmNZAKwmhRQtS9wGYKQj8cJaPpK2xaDzx3RppQ8PsM/rQ9/p0Lme+x75dozBEbmFn6Q9eCXOCiCivVsKzZ8Vz";
The Java-method decrypt will provide the initial plain text.
A final note: If you decide to remove the (redundant) custom padding replace in the PHP-code the line
$encryptedData = openssl_encrypt(pad($data, 16), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
with
$encryptedData = openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
and remove the pad- and the unpad-method.
In the Java-code replace the lines
// Remove custom padding
byte[] unpaddedData = unpad(decryptedData);
return new String(unpaddedData);
with
return new String(decryptedData);
and remove the unpad-method.
Edit 3:
The InvalidKeyException (Illegal key size) mentioned in the Edit2-section of the question is already discussed e.g. here InvalidKeyException Illegal key size and here Java Security: Illegal key size or default parameters?.

Related

PHP AES-256-CBC encrypted data is different from JAVA AES/CBC/PKCS5PADDING

I have a java code working perfectly
public static String encrypt(String message, String sercretKey)
{
String base64EncryptedString = "";
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digestOfPassword = md.digest(sercretKey.getBytes("utf-8"));
byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
byte[] iv = Arrays.copyOf(digestOfPassword, 16);
SecretKey key = new SecretKeySpec(keyBytes, "AES");
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key, ivParameterSpec);
byte[] plainTextBytes = message.getBytes("utf-8");
byte[] buf = cipher.doFinal(plainTextBytes);
byte[] base64Bytes = Base64.getEncoder().encode(buf);
base64EncryptedString = new String(base64Bytes);
return base64EncryptedString;
}
I have tried using below code to recreate this above code in PHP
function encryptTest($sSecretKey,$sValue)
{
$key = hash('sha256', $sSecretKey,false);
$key = utf8_encode($key);
$key = substr($key, 0, 24);
$iv = substr($key, 0, 16);
$data = $sValue;
$outEnc = openssl_encrypt($data, "AES-256-CBC", $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($outEnc);
}
But showing different results. What I have missed.
(Same types of questions are available in StackOverflow, but pointing my issues)
There are the following issues:
In the PHP code, the key is currently returned hex encoded. Instead, it must be returned as bytes string. To do this, the third parameter in hash() must be switched from false to true.
In the Java code a 192 bits key is used, i.e. AES-192. Accordingly, in the PHP code "AES-192-CBC" must be applied (and not "AES-256-CBC").
The utf8_encode() call in the PHP code is to be removed, as this corrupts the key.
With these changes, both codes provide the same ciphertext.
Security:
Using SHA256 as key derivation is insecure. Instead apply a dedicated algorithm like Argon2 or PBKDF2. Also, using the key (or a part of it) as IV is insecure as it results in the reuse of key/IV pairs. Instead, a randomly generated IV should be applied for each encryption.

How to decrypt a cipher text using Java which is encrypted with Rijndael-256 with custom IV?

I have found a source code at stackoverflow on Rinjndael-256 encryption and decryption written in c#. It is using custom IV appending some extra string. Now I need a decryption method on Java platform. I have found some source code; tried to change and test that. Here is the encryption method on c#:
public static string Encrypt(byte[] text, string key)
{
RijndaelManaged aes = new RijndaelManaged();
aes.KeySize = 256;
aes.BlockSize = 256;
aes.Padding = PaddingMode.None;
aes.Mode = CipherMode.CBC;
aes.Key = Encoding.Default.GetBytes(key);
aes.GenerateIV();
string IV = ("-[--IV-[-" + Encoding.Default.GetString(aes.IV));
ICryptoTransform AESEncrypt = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] buffer = text;
return Convert.ToBase64String(Encoding.Default.GetBytes(Encoding.Default.GetString(AESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length)) + IV));
}
The decryption method on java which is not working for me is:
public static String decrypt(byte[] cipherText, String encryptionKey) throws Exception{
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", "SunJCE");
SecretKeySpec key = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES");
cipher.init(Cipher.DECRYPT_MODE, key,new IvParameterSpec(IV.getBytes("UTF-8")));
return new String(cipher.doFinal(cipherText),"UTF-8");
}
Edit 1:
I have implemented the php code for decryption.
function decrypt($text, $pkey)
{
$key = $pkey;
$text = base64_decode($text);
$IV = substr($text, strrpos($text, "-[--IV-[-") + 9);
$text = str_replace("-[--IV-[-" . $IV, "", $text);
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $text, MCRYPT_MODE_CBC, $IV), "\0");
}
Is there any manual way of implementing Rijndael-256 in Java? As someone said that
There is no support in any of the Sun JCE providers for anything other than Rijndael with the 128-bit blocksize
I don't have an option to use library

Removing characters from decrypted Base64 string in Java

In relation to this question; Java (Android) Decrypting msg with IV attached
My message decrypts fine but with the unwanted IV byte data.
I try to remove the attached IV, yet it does not delete all the characters and some character always remain behind. I'm not sure how I should be calculating the length of the encoded IV to remove the non-required characters.
public String decrypt(String cipherText, byte[] encryptionKey) throws Exception {
SecretKeySpec key = new SecretKeySpec(encryptionKey, "AES");
cipher.init(Cipher.DECRYPT_MODE, key, iV);
String decrypt = new String(cipher.doFinal( Base64.decode(cipherText, Base64.DEFAULT)));
byte[] decryptData = new byte[decrypt.getBytes().length - iV.getIV().length];
System.arraycopy(decrypt.getBytes(), iV.getIV().length, decryptData, 0, decrypt.getBytes().length - iV.getIV().length);
Log.d("decrypt = ", decrypt);
decrypt = new String(decryptData, "UTF-8");
return decrypt;
}
You need to remove the IV before decryption not after it, because it is a parameter of the decryption. Since the IV is prepended to the ciphertext there is no need to save it somewhere else (no need for your iV reference).
byte[] ciphertextBytes = Base64.decode(cipherText, Base64.DEFAULT);
IvParameterSpec iv = new IvParameterSpec(ciphertextBytes, 0, 16);
ciphertextBytes = Arrays.copyOfRange(ciphertextBytes, 16, ciphertextBytes.length);
SecretKeySpec key = new SecretKeySpec(encryptionKey, "AES");
cipher.init(Cipher.DECRYPT_MODE, key, iv);
String decrypt = new String(cipher.doFinal(ciphertextBytes), "UTF-8");

Trying to translate AES CTR decrypting Python code to java

This is the code in question:
decrypt(self):
"""Decrypt one block"""
data = self.source.read(1024)
if not data:
return ""
iv = data[:16]
encrypted = data[16:]
counter = Crypto.Util.Counter.new(64, prefix=iv[:8], initial_value=struct.unpack(">Q", iv[8:])[0])
cipher = Crypto.Cipher.AES.new(self.info["CpData"], Crypto.Cipher.AES.MODE_CTR, counter=counter)
return cipher.decrypt(encrypted)
This is the line I have problems understanding:
counter = Crypto.Util.Counter.new(64, prefix=iv[:8], initial_value=struct.unpack(">Q", iv[8:])[0])
What does it do and how do I replicate it in Java? Currently I have this, but the result is not what I expect:
public static byte[] decrypt(byte[] encryptedData) throws Exception {
Key key = new SecretKeySpec(keyBytes, "AES");
Cipher c = Cipher.getInstance("AES/CTR/NoPadding");
byte[] iv = Arrays.copyOfRange(encryptedData, 0, 16) ; // first 16 bytes
byte[] data = Arrays.copyOfRange(encryptedData, 16, 1024); // rest
IvParameterSpec ivSpec = new IvParameterSpec(iv);
c.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decValue = c.doFinal(data);
return decValue;
}
Hours of googling have not yielded a useable result. How do I use this counter thing in Java?
Thanks!
On a quick look it appears that there is 8 bytes of IV prepended to the incoming cyphertext. Extract the first 8 bytes and use them as an IV to set up AES-CTR. Then decrypt the rest of the cyphertext.

Java encrypt/decript data from PHP to Java, IllegalBlockSizeException

I'm trying to read a base64 encoded and AES 128-bit encrypted string from PHP, but I'm getting IllegalBlockSizeException.
PHP encrypt:
encrypt("My f awesome test !");
function encrypt($string){
$td = mcrypt_module_open('rijndael-128', '', 'cbc', "1cc251f602cf49f2");
mcrypt_generic_init($td, "f931c96c4a4e7e47", "1cc251f602cf49f2");
$enc = mcrypt_generic($td, $string);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return base64_encode($enc);
}
And the returned value is:
McBeY73GQ5fawxIunVKpqUupipeRlt9ntyMRzjbPfTI=
Now I want to read it in Java:
static public String decrypt(String data) throws Exception {
data = new String( Base64.decode(data, Base64.NO_WRAP) );
byte[] keyByte = "f931c96c4a4e7e47".getBytes("UTF-8");
byte[] ivByte = "1cc251f602cf49f2".getBytes("UTF-8");
Key key = new SecretKeySpec(keyByte, "AES");
IvParameterSpec iv = new IvParameterSpec(ivByte);
Cipher c = Cipher.getInstance("AES/CBC/NoPadding");
c.init(Cipher.DECRYPT_MODE, key, iv);
byte[] bval = c.doFinal( data.getBytes("UTF-8") );
return new String( bval );
}
And I'm getting an Exception:
javax.crypto.IllegalBlockSizeException: data not block size aligned
This might be caused by padding?
EDIT
Your error was caused by the conversion of the plaintext to and from a string. It's not necessary anyway - just use byte arrays:
byte[] data = Base64
.decodeBase64("McBeY73GQ5fawxIunVKpqUupipeRlt9ntyMRzjbPfTI=");
byte[] keyByte = "f931c96c4a4e7e47".getBytes("UTF-8");
byte[] ivByte = "1cc251f602cf49f2".getBytes("UTF-8");
Key key = new SecretKeySpec(keyByte, "AES");
IvParameterSpec iv = new IvParameterSpec(ivByte);
Cipher c = Cipher.getInstance("AES/CBC/NoPadding");
c.init(Cipher.DECRYPT_MODE, key, iv);
byte[] bval = c.doFinal(data);
System.out.println(new String(bval)); // Prints My f awesome test !
I recommend you use padding in your encryption, otherwise you cannot cope with arbitrarily-sized input.
the IllegalBlockSizeException thrown on call to doFinal() if: "cipher is a block cipher, no padding has been requested (only in encryption mode), and the total input length of the data processed by this cipher is not a multiple of block size; or if this encryption algorithm is unable to process the input data provided." -http://docs.oracle.com/javase/6/docs/api/javax/crypto/Cipher.html#doFinal%28%29. So its either bad input data or block size.
This is a working version of encryption/decryption between
https://github.com/chaudhuri-ab/CrossPlatformCiphers
Hope this helps someone as it took me a while to work through the little details between the platforms.

Categories