java.security.InvalidKeyException: Parameters missing - java

I need to store the encrypted password in DB without storing any key or salt. And I want to make sure it's safer as well even though it's a two-way encryption. So after googling a bit, I have created a sample program to test it. My idea is to create a custom JPA AttributeConverter class to manage this.
Following is the program:
import java.security.Key;
import java.security.spec.KeySpec;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class Crypto {
private static final String _algorithm = "AES";
private static final String _password = "_pasword*";
private static final String _salt = "_salt*";
private static final String _keygen_spec = "PBKDF2WithHmacSHA1";
private static final String _cipher_spec = "AES/CBC/PKCS5Padding";
public static String encrypt(String data) throws Exception {
Key key = getKey();
System.out.println(key.toString());
Cipher cipher = Cipher.getInstance(_cipher_spec);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encVal = cipher.doFinal(data.getBytes());
String encryptedValue = Base64.getEncoder().encodeToString(encVal);
System.out.println("Encrypted value of "+data+": "+encryptedValue);
return encryptedValue;
}
public static void decrypt(String encryptedData) throws Exception {
Key key = getKey();
System.out.println(key.toString());
Cipher cipher = Cipher.getInstance(_cipher_spec);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decordedValue = Base64.getDecoder().decode(encryptedData);
byte[] decValue = cipher.doFinal(decordedValue);
String decryptedValue = new String(decValue);
System.out.println("Decrypted value of "+encryptedData+": "+decryptedValue);
}
private static Key getKey() throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance(_keygen_spec);
KeySpec spec = new PBEKeySpec(_password.toCharArray(), _salt.getBytes(), 65536, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), _algorithm);
return secret;
}
public static void main(String []str) throws Exception {
String value = encrypt("India#123");
decrypt(value);
}
}
But its throwing the following exception:
javax.crypto.spec.SecretKeySpec#17111
Encrypted value of India#123: iAv1fvjMnJqilg90rGztXA==
javax.crypto.spec.SecretKeySpec#17111
Exception in thread "main" java.security.InvalidKeyException: Parameters missing
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:469)
at com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:313)
at javax.crypto.Cipher.implInit(Cipher.java:802)
at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
at javax.crypto.Cipher.init(Cipher.java:1249)
at javax.crypto.Cipher.init(Cipher.java:1186)
at org.lp.test.Crypto.decrypt(Crypto.java:37)
at org.lp.test.Crypto.main(Crypto.java:54)
I am not able to figure this out.
I have rectified the exception based on #Luke Park's answer I have created a JPA AttributeConverter as below:
import java.security.Key;
import java.security.spec.KeySpec;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter(autoApply=true)
public class CryptoJPAConverter implements AttributeConverter<String, String> {
private static final String _algorithm = "AES";
private static final String _password = "_pasword*";
private static final String _salt = "_salt*";
private static final String _keygen_spec = "PBKDF2WithHmacSHA1";
private static final String _cipher_spec = "AES/ECB/PKCS5Padding";
#Override
public String convertToDatabaseColumn(String clearText) {
Key key;
Cipher cipher;
try {
key = getKey();
cipher = Cipher.getInstance(_cipher_spec);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encVal = cipher.doFinal(clearText.getBytes());
String encryptedValue = Base64.getEncoder().encodeToString(encVal);
return encryptedValue;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
#Override
public String convertToEntityAttribute(String encryptedText) {
Key key;
try {
key = getKey();
Cipher cipher = Cipher.getInstance(_cipher_spec);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decordedValue = Base64.getDecoder().decode(encryptedText);
byte[] decValue = cipher.doFinal(decordedValue);
String decryptedValue = new String(decValue);
return decryptedValue;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static Key getKey() throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance(_keygen_spec);
KeySpec spec = new PBEKeySpec(_password.toCharArray(), _salt.getBytes(), 65536, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), _algorithm);
return secret;
}
}
I have used the two way encryption because I need to pass the password as clear text to Java mail client.
Suggestion and comments are welcome

You are using AES/CBC/PKCS5Padding but you are not passing an IV to your cipher.init calls.
CBC mode requires a random IV for each encryption operation, you should generate this using SecureRandom each time you encrypt, and pass the value in as an IvParameterSpec. You'll need the same IV for decryption. It is common to prepend the IV to the ciphertext and retrieve when required.
On a seperate note, encrypting passwords is really quite a terrible idea. The fact that you have to ask this question at all somewhat proves that you aren't in a position to be making security-related decisions. Do yourself and your project a favour and hash your passwords instead. PBKDF2 and bcrypt are both decent ways of doing so.

Related

String Encrypted in Java need to Decrypt in Angular 6 using AES-256-GCM

I have Encrypted String in Java using AES/GCM/NoPadding.Now I want to decrypt in Angular6. All examples I have found are using AES-CBC, I want to Decrypt with AES-GCM.
Here is my Java code.
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AESGCMEncryptionUtils {
private static final String SALT = "some salt";
private static final int AES_KEY_SIZE = 256;
private static final int GCM_NONCE_LENGTH = 12; // IV
private static final int GCM_TAG_LENGTH = 16;
private static final String AES_GCM_ALGORITHM = "AES/GCM/NoPadding";
private static final String PBKDF2 = "PBKDF2WithHmacSHA256";
private static final String ALGORITHAM = "AES";
public static SecretKeySpec generateKey() throws Exception
{
PBEKeySpec spec = new PBEKeySpec("some password".toCharArray(),SALT.getBytes(),10000, AES_KEY_SIZE);
SecretKey key = SecretKeyFactory.getInstance(PBKDF2).generateSecret(spec);
SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), ALGORITHAM);
return keySpec;
}
public static String encrypt(String strToEncrypt)
{
try
{
byte[] nonce = new byte[GCM_NONCE_LENGTH];
Cipher cipher = Cipher.getInstance(AES_GCM_ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8 , nonce);
cipher.init(Cipher.ENCRYPT_MODE, generateKey(), gcmParameterSpec);
return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8")));
}
catch (Exception e)
{
log.info("Error while encrypting: " + e.toString());
return null;
}
}
}
I need equivalent Decryption logic in Angular6. Please help me on this.

AesEncryption cause different result in Java And PHP code,Why?

I am connecting a third party interface and they only gave me a Java code demo, I need to connect it with my PHP system.But I can't handle the encryption, my encryption proccess in PHP always gets different result with the third party Java code.
I install phpseclib/phpseclib package via composer in order to perform AES encryption.
Java Encryption Code:
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
private static final String KEY_ALGORITHM = "AES";
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
public static String encrypt(String content, String encryptKey) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(encryptKey));
byte[] result = cipher.doFinal(byteContent);
return toHexString(result);
} catch (Exception ex) {
}
return null;
}
private static SecretKeySpec getSecretKey(final String encryptKey) {
KeyGenerator kg = null;
try {
kg = KeyGenerator.getInstance(KEY_ALGORITHM);
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(encryptKey.getBytes());
kg.init(128, secureRandom);
SecretKey secretKey = kg.generateKey();
return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
} catch (NoSuchAlgorithmException ex) {
}
return null;
}
public static String toHexString(byte b[]) {
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < b.length; i++) {
String plainText = Integer.toHexString(0xff & b[i]);
if (plainText.length() < 2)
plainText = "0" + plainText;
hexString.append(plainText);
}
return hexString.toString();
}
PHP encryption code:
//composer require phpseclib/phpseclib
use phpseclib\Crypt\AES;
function aesEncrypt($message, $key)
{
$cipher = new AES(AES::MODE_ECB);
$cipher->setKeyLength(128);
$cipher->setKey(hex2bin($key));
$cryptText = $cipher->encrypt($message);
return bin2hex($cryptText);
}
Java Result:
before:testStringtestString
key:acccd6fa0caf52a0e5e5fda8bd3ff55a
after:2bbd3011eb084c9494228fe913e6e033aaffb1aa04ef9d0f14614c21fd16af9a
PHP Result:
before:testStringtestString
key:acccd6fa0caf52a0e5e5fda8bd3ff55a
after:d9a683511cdeb174bf51a285140071a8b38b57c6c6133d1b9425846ae0ec333b

What is the difference between These method below? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 5 years ago.
Improve this question
i am learning encryption in java and used these two tutorials yet This from Code2learn
and This from howtodoinjava.com
when i try to encrypt hello from these example code both gives different encrypted strings, i have tried a lot to find that what is the main difference between these examples that is causing different encrypted strings of same passwords as both are using AES ..
Code from code 2 learn
package nomad;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.*;
import sun.misc.*;
public class AESencrp {
private static final String ALGO = "AES";
private static final byte[] keyValue =
new byte[] { 'T', 'h', 'e', 'B', 'e', 's', 't',
'S', 'e', 'c', 'r','e', 't', 'K', 'e', 'y' };
public static String encrypt(String Data) throws Exception {
Key key = generateKey();
Cipher c = Cipher.getInstance(ALGO);
c.init(Cipher.ENCRYPT_MODE, key);
byte[] encVal = c.doFinal(Data.getBytes());
String encryptedValue = new BASE64Encoder().encode(encVal);
return encryptedValue;
}
public static String decrypt(String encryptedData) throws Exception {
Key key = generateKey();
Cipher c = Cipher.getInstance(ALGO);
c.init(Cipher.DECRYPT_MODE, key);
byte[] decordedValue = new BASE64Decoder().decodeBuffer(encryptedData);
byte[] decValue = c.doFinal(decordedValue);
String decryptedValue = new String(decValue);
return decryptedValue;
}
private static Key generateKey() throws Exception {
Key key = new SecretKeySpec(keyValue, ALGO);
return key;
}
}
package nomad;
public class Checker {
public static void main(String[] args) throws Exception {
String password = "mypassword";
String passwordEnc = AESencrp.encrypt(password);
String passwordDec = AESencrp.decrypt(passwordEnc);
System.out.println("Plain Text : " + password);
System.out.println("Encrypted Text : " + passwordEnc);
System.out.println("Decrypted Text : " + passwordDec);
}
}
Code for how to do in java
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class AES {
private static SecretKeySpec secretKey;
private static byte[] key;
public static void setKey(String myKey)
{
MessageDigest sha = null;
try {
key = myKey.getBytes("UTF-8");
sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
key = Arrays.copyOf(key, 16);
secretKey = new SecretKeySpec(key, "AES");
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public static String encrypt(String strToEncrypt, String secret)
{
try
{
setKey(secret);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8")));
}
catch (Exception e)
{
System.out.println("Error while encrypting: " + e.toString());
}
return null;
}
public static String decrypt(String strToDecrypt, String secret)
{
try
{
setKey(secret);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)));
}
catch (Exception e)
{
System.out.println("Error while decrypting: " + e.toString());
}
return null;
}
}
public static void main(String[] args)
{
final String secretKey = "ssshhhhhhhhhhh!!!!";
String originalString = "howtodoinjava.com";
String encryptedString = AES.encrypt(originalString, secretKey) ;
String decryptedString = AES.decrypt(encryptedString, secretKey) ;
System.out.println(originalString);
System.out.println(encryptedString);
System.out.println(decryptedString);
}
I have added the codes as one of the comments says that external links are not accessible by everyone .
Keep in mind that secretkeys and passowrd are the same in my code.
Adding my own code..
name it as code 1
AES Class
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
public class AES {
public static SecretKeySpec secretKeySpec;
private static byte[] key;
public static String setKey(String myKey) throws Exception {
MessageDigest sha=null;
key =myKey.getBytes("UTF-8");
sha=MessageDigest.getInstance("SHA-1");
key=sha.digest(key);
key= Arrays.copyOf(key,16);
secretKeySpec=new SecretKeySpec(key,"AES");
return myKey;
}
public static String encrypt(String strToencrypt, String secret) throws Exception {
String s= setKey(secret);
System.out.println(s);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec);
return Base64.getEncoder().encodeToString(cipher.doFinal(strToencrypt.getBytes("UTF-8")));
}
public static String decryt(String strToDec , String secret) throws Exception {
setKey(secret);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE,secretKeySpec);
return new String(cipher.doFinal(Base64.getDecoder().decode(strToDec)));
}
}
Main Class of above AES Class
public class Main {
public static void main(String[] args) throws Exception {
AES aes = new AES();
String ss=null;
String sd=null;
ss=aes.encrypt("hello","TheBestSecretKey");
sd=aes.decryt(ss,"TheBestSecretKey");
System.out.println("The Encrypted == " + ss);
System.out.println("The Decrypted == " + sd);
}
}
Another Code
Name It as Code 2
package sample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
public class Main {
private static final String ALGO ="AES";
private static String string = "TheBestSecretKey";
private static byte[] key ;
private static SecretKeySpec secretKeySpec;
public static String aesEncrypt(String en) throws Exception {
Key key = new SecretKeySpec(string.getBytes(),ALGO);
Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE,key);
byte[] encValue =c.doFinal(en.getBytes("UTF-8"));
String encryptedValue= new BASE64Encoder().encode(encValue);
return encryptedValue;
}
public static String aesDecrypt(String De) throws Exception{
Key key = new SecretKeySpec(string.getBytes(),"AES");
Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
c.init(Cipher.DECRYPT_MODE,key);
// return new String(c.doFinal(Base64.getDecoder().decode(De)));
byte[]decodedVlue=new BASE64Decoder().decodeBuffer(De);
byte[] decValue = c.doFinal(decodedVlue);
String deccryptedValue = new String(decValue);
return deccryptedValue;
}
public static void main(String[] args) throws Exception {
String password = "hello";
String passEnc= Main.aesEncrypt(password);
System.out.println(passEnc);
String passDec = Main.aesDecrypt(passEnc);
System.out.println(passDec);
}
}
here are the codes
now i am also posting encrypted strings
using SecretSpecKey TheBestSecretKey and hello as the string to be encrypted
CODE 1 UlQiIs/K0EwcSKbWMjcT1g==
CODE 2 hHDBo1dJYj5RcjdFA6BBfw==

ECB and CBC AES output is equal in Java

I've played around with the Java AES En/Decryption and used different cyper modes for this. Namely I use CBC and ECB. As ECB is considered to be weak, I wanted to go with CBC.
I assumed the output of the encrypted texts ob cbc and ecb are different, but they are equal. How is this possible?
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
import com.instana.backend.common.exception.InstanaException;
public class AESTest {
private static String pwd = "etjrgp9user9fu3984h1&(/&%$ยง";
public static void main(String[] args) throws Exception {
System.out.println("UNSECURE WITH ECB:");
String ecbEncrypt = encrypt("YOLO", cypher(Cipher.ENCRYPT_MODE, "AES"));
System.out.println("Encrypted: " + ecbEncrypt);
String ebcDecrypt = decrypt(ecbEncrypt, cypher(Cipher.DECRYPT_MODE, "AES"));
System.out.println("Decrypted: " + ebcDecrypt);
System.out.println("=====================================");
System.out.println("SECURE WITH CBC");
String cbcEncrypt = encrypt("YOLO", cypher(Cipher.ENCRYPT_MODE, "AES/CBC/PKCS5Padding"));
System.out.println("Encrypted: " + cbcEncrypt);
String cbcDecrypt = decrypt(cbcEncrypt, cypher(Cipher.DECRYPT_MODE, "AES/CBC/PKCS5Padding"));
System.out.println("Decrypted: " + cbcDecrypt);
System.out.println("=====================================");
System.out.println("Decrypting CBC with ECB");
}
public static String encrypt(String superDuperSecret, Cipher cipher) throws IOException {
try {
byte[] encrypted = cipher.doFinal(superDuperSecret.getBytes("UTF-8"));
return new String(new Hex().encode(encrypted));
} catch (Exception e) {
throw new InstanaException("Encryption of token failed.", e);
}
}
public static String decrypt(String superDuperSecret, Cipher cipher) {
try {
byte[] encrypted1 = new Hex().decode(superDuperSecret.getBytes("UTF-8"));
return new String(cipher.doFinal(encrypted1));
} catch (Exception e) {
throw new InstanaException("Encrypted text could not be decrypted.", e);
}
}
private static Cipher cypher(int mode, String method)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException,
InvalidAlgorithmParameterException {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(pwd.toCharArray(), pwd.getBytes(), 128, 128);
SecretKey tmp = skf.generateSecret(spec);
SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance(method);
if(method.contains("CBC")) {
byte[] ivByte = new byte[cipher.getBlockSize()];
IvParameterSpec ivParamsSpec = new IvParameterSpec(ivByte);
cipher.init(mode, key, ivParamsSpec);
}else{
cipher.init(mode, key);
}
return cipher;
}
}
Since you're passing an empty IV (you never put anything inside your ivByte), the operations performed for the first block are identical regardless of the mode being used. Encrypting a longer payload would result in the second block being chained to the first block in the case of CBC and the following blocks would be different between ECB/CBC.
You should pass a non-empty IV when using CBC mode, so the first block will be xorred with the IV, resulting in different encrypted values starting from the first block.

What is the encryption used in this java code? Any PHP equavent for this

I have a java code
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import java.io.PrintStream;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class SecureCardData
{
public static final String retailerid = "61220121";
public String encryptData(String sData)
throws Exception
{
byte[] bPrivateKey = "61220121".getBytes();
SecretKeySpec spec = new SecretKeySpec(bPrivateKey, "DES");
Cipher cipher = Cipher.getInstance("DES");
cipher.init(1, spec);
byte[] bEncryptedData = cipher.doFinal(sData.getBytes());
return Base64.encode(bEncryptedData);
}
public String decryptData(String sData)
throws Exception
{
byte[] bPrivateKey = "61220121".getBytes();
SecretKeySpec spec = new SecretKeySpec(bPrivateKey, "DES");
Cipher cipher = Cipher.getInstance("DES");
cipher.init(2, spec);
byte[] bencryptedData = Base64.decode(sData);
byte[] bDecryptedData = cipher.doFinal(bencryptedData);
return new String(bDecryptedData);
}
public static void main(String[] args)
throws Exception
{
String s = "1800585544448888|445|0611";
SecureCardData sd = new SecureCardData();
String ss = sd.encryptData(s);
System.out.println(ss);
ss = sd.decryptData(ss);
System.out.println(ss);
}
}
Im not into java,can anyone help me make the code equivalent for this in php?
I haven't touched java ever.Finding it really hard to figure out.Is there someone who could help me with this code.?
new SecretKeySpec(bPrivateKey, "DES");
This creates a secret key using the DES specification.
http://en.wikipedia.org/wiki/Data_Encryption_Standard
Note that DES is considered obsolete/easy to break so has not been recommended for use in quite some time.

Categories

Resources