Bouncycastle in Java odd encryption and decryption results - java

So I have been working with the Bouncycastle libraries in an attempt to connect with a remote server. This process has been problematic from the get go and now I'm close to getting everything working but some odd things are happening.
When I first started building out the encryption process I was told to use AES 256 with PKCS7Padding. After some nagging I was provided with a c++ example of the server code. It turned out that the IV is 256 bit so I had to use the RijndaelEngine instead. Also in order for this to work correctly I have to use ZeroBytePadding.
Here is my code:
socket = new Socket(remoteIP, port);
outputStream = new PrintWriter(socket.getOutputStream());
inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
byte[] base_64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes("UTF-8");
Security.addProvider(new BouncyCastleProvider());
public String AESEncrypt(String out) throws IOException, DataLengthException, IllegalStateException, InvalidCipherTextException {
byte[] EncKey = key;
byte randKey;
Random randNumber = new Random();
randKey = base_64[randNumber.nextInt(base_64.length)];
EncKey[randKey&0x1f] = randKey;
RijndaelEngine rijndaelEngine = new RijndaelEngine(256);
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(rijndaelEngine), new ZeroBytePadding());
ParametersWithIV keyParameter = new ParametersWithIV(new KeyParameter(EncKey), iv);
cipher.init(true, keyParameter);
byte[] txt = out.getBytes();
byte[] encoded = new byte[cipher.getOutputSize(txt.length)];
int len = cipher.processBytes(txt, 0, txt.length, encoded, 0);
cipher.doFinal(encoded, len);
char keyChar = (char) randKey;
String encString = new String(Base64.encode(encoded));
encString = encString.substring(0, encString.length()-1) + randKey;
return encString;
}
public void AESDecrypt(String in) throws DataLengthException, IllegalStateException, IOException, InvalidCipherTextException {
byte[] decKey = key;
byte[] msg = in.getBytes();
byte randKey = msg[msg.length-1];
decKey[randKey&0x1f] = randKey;
byte[] trimMsg = new byte[msg.length-1];
System.arraycopy(msg, 0, trimMsg, 0, trimMsg.length);
in = new String(trimMsg);
RijndaelEngine rijndaelEngine = new RijndaelEngine(256);
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(rijndaelEngine), new ZeroBytePadding());
ParametersWithIV keyParameter = new ParametersWithIV(new KeyParameter(decKey), iv);
cipher.init(false, keyParameter);
byte[] encoded = Base64.decode(in.trim());
byte[] decoded = new byte[cipher.getOutputSize(encoded.length)];
int len = cipher.processBytes(encoded, 0, encoded.length, decoded, 0);
cipher.doFinal(decoded, len);
String decString = new String(decoded);
}
Here is a test function I am using to send and receive messages:
public void serverTest() throws DataLengthException, IllegalStateException, InvalidCipherTextException, IOException {
//out = AESEncrypt(out);
outputStream.write(out + "\n");
outputStream.flush();
String msg = "";
while ((msg = inputStream.readLine()) != null) {
AESDecrypt(msg);
}
}
The key and iv don't change with the exception of the last byte in the key. If I am encrypting I get a random base64 char and change the last byte to that. If its decryption I get the last byte from the message and set the last value of the key to it for decryption.
In the c++ example there was an unencrypted message and two encrypted messages. I could deal with those fine.
Here is the problem, when I send my message to the remote server "encrypted" the app waits for a response until the connection times out but never gets one. If I send the message unencrypted I get either 7 responses which I can successfully decrypt and finally
org.bouncycastle.util.encoders.DecoderException: unable to decode base64 string:
String index out of range: -4 at org.bouncycastle.util.encoders.Base64.decode(Unknown Source)
or my last line before the error will look like this:
?"??n?i???el????s???!_S=??ah????CR??l6??]?{?l??Y?????Gn???+?????9!'??gU&4>??{X????G?.$c=??0?5??GP???_Q5????8??Z\?~???<Kr?????[2\ ???a$?C??z%?W???{?.?????eR?j????~?B"$??"z??W;???<?Yu??Y*???Z?K?e!?????f?;O(?Zw0B??g<???????????,)?L>???A"?????<?????W??#\???f%??j ?EhY/?? ?5R?34r???#?1??I??????M
If I set the encryption/decryption to use PKCS7Padding I get no response when my message is encrypted still but with decryption from the server I get between 2 to 6 responses and then
org.bouncycastle.crypto.InvalidCipherTextException: pad block corrupted
I am at a loss with this. I don't know what I might be doing wrong so I have come here. I'm hoping the so community can point out my errors and guide me in the right direction.
I have a bit of an update I found my error in the encryption. I wasn't placing the random base64 value at the end of the encrypted string correctly so now I am doing like this.
encString += (char)randKey;
I can get response from the server now. Now the problem is I will some times get one or two readable lines but the rest are all garbage. I asked the individuals who run the server about it and they said in some c# code that they reference the have
return UTF8Encoding.UTF8.GetString(resultArray);
and thats all I have to go off of. I have tried UTF-8 encoding any place where I do getBytes or new String, and I have tried making the BurrferReader stream UTF-8 but it's still garbage.

Have you seedn the BCgit? this has bouncycastle code and examples. I am using the Csharp version in this repository. https://github.com/bcgit/bc-java
All crypto primitive examples are stored here: https://github.com/bcgit/bc-java/tree/master/core/src/test/java/org/bouncycastle/crypto/test
Try this code for testing Aes-CBC
private void testNullCBC()
throws InvalidCipherTextException
{
BufferedBlockCipher b = new BufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
KeyParameter kp = new KeyParameter(Hex.decode("5F060D3716B345C253F6749ABAC10917"));
b.init(true, new ParametersWithIV(kp, new byte[16]));
byte[] out = new byte[b.getOutputSize(tData.length)];
int len = b.processBytes(tData, 0, tData.length, out, 0);
len += b.doFinal(out, len);
if (!areEqual(outCBC1, out))
{
fail("no match on first nullCBC check");
}
b.init(true, new ParametersWithIV(null, Hex.decode("000102030405060708090a0b0c0d0e0f")));
len = b.processBytes(tData, 0, tData.length, out, 0);
len += b.doFinal(out, len);
if (!areEqual(outCBC2, out))
{
fail("no match on second nullCBC check");
}
}

Related

C# encrypted data getting truncated using Java decryption code

All,I am posting some encrypted xml data(Using AES-128 ) to another application that uses Java to decrypt.When the Java code decrypts the xml,the start tag of the xml is getting truncated and fails validation.I don't have access to their code base .I can decrypt the same data using C# without any data loss.Please see the code I use to encrypt and Decrypt the data . I have researched this and based on the research ,I added the FlushFinalBlocks() and Close() to the CryptoStream in the encryption logic ,but this doesnt seem to fix the issue.
Encryption Code:
public static string Aes128Encrypt(string plainText)
{
string encodedPayload = null;
string base64Iv = null;
string base64Key = null;
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
using (RijndaelManaged aesAlg = new RijndaelManaged())
{
aesAlg.KeySize = 128;
aesAlg.Mode = CipherMode.CBC;
aesAlg.Padding = PaddingMode.PKCS7;
aesAlg.BlockSize = 128;
base64Iv = Convert.ToBase64String(aesAlg.IV);
base64Key = Convert.ToBase64String(aesAlg.Key);
// Create a decrytor to perform the stream transform.
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
csEncrypt.Write(plainBytes, 0, plainBytes.Length);
csEncrypt.FlushFinalBlock();
encodedPayload = Convert.ToBase64String(msEncrypt.ToArray());
csEncrypt.Close();
}
msEncrypt.Flush();
msEncrypt.Close();
}
}
return encodedPayload ;
}
Decryption Code:
public static string Aes128Decrypt(string base64Key, string base64IV, string encodedPayload)
{
string plainText = null;
byte[] key = Convert.FromBase64String(base64Key);
byte[] iv = Convert.FromBase64String(base64IV);
byte[] encryptedBytes = Convert.FromBase64String(encodedPayload);
using (RijndaelManaged aesAlg = new RijndaelManaged())
{
aesAlg.KeySize = 128;
aesAlg.Mode = CipherMode.CBC;
aesAlg.BlockSize = 128;
aesAlg.Padding = PaddingMode.PKCS7;
aesAlg.Key = key;
aesAlg.IV = iv;
// Create a decrytor to perform the stream transform.
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for decryption.
using (MemoryStream msDecrypt = new MemoryStream(encryptedBytes))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
plainText = srDecrypt.ReadToEnd();
}
}
}
}
return plainText;
}
Testing Code:
string textXml = #"<person>
<firstName>Rennish</firstName>
<lastName>Joseph</lastName>
<accountNumber>12345678910</accountNumber>
<ssn>123456</ssn>
</person>";
Aes128Encrypt(textXml);
string encodedPayload = "4p6uU7SiqB0uCzsrWXMOStP02HM7mKA6QVzcKoNdu3w1+MYLjYVbW/Ig3XPKRRafeu+WKDMuKJJaEREkrZt/Ycvc50wfe2naJ9d0UT5B7Fre1gIsNfZUIK3SF304+WF8zX730mVsluJABKT3JCkk9AkOGCQWPYzcZvH9dojIqGP7V+2j1+IMOPMWWFIitkAi8B7ALxMuMcepzX2/cxHxH7NeID0ytEGUzGfJXSAzQcvBX9dWwUqdMX3Eip5SRPMsotnWWsFTjDuOiZk/q5fuxxWbS6cuYn/64C/vQjEIuheQKn0ZOIDLNPCUavvWD2u6PWNKMNgW/qUIq13W9PQxzIiQxrT7ZqPFJu75C1KdXXUG5lghU7EBAGehHC/5BqFjs9SuYJkV1RrchMEzytrJIQ7Zp4CnOU6Q1rEhFTaMk/s=";
string encodedKey = "2zpVbIxqvjSfJo7zkXzl2A==";
string encodedIV = "5WOQPdmB/BkECmuPdNTaLw==";
Aes128Decrypt(encodedKey, encodedIV, encodedPayload);
Data after encryption at the JAVA application looks like this
<rson>
<firstName>Rennish</firstName>
<lastName>Joseph</lastName>
<accountNumber>12345678910</accountNumber>
<ssn>123456</ssn>
</person>
Interesting problem.
I think the encryption and decryption works fine on both sides.
If part of the encrypted message was lost in transmission you would not be able to decrypt it due to the avalanche effect. So it appears that characters go missing in the plain text.
This might be an encoding issue in the plain text message. The bytes you have encoded and the bytes they decoded are probably the same. The way they are interpreted might not be.
Now there are two options here:
Either <person> becomes <rson> or it becomes rson> and there was a copy-paste mistake.
If the latter case is true then we're missing 3 bytes. This makes me think that the protocol might presume the presence of a byte order marker andsimply removes the first 3 bytes to get rid of it.
If the former case you'd have some very weird encoding issues. As all missing characters appear to be in the ascii range so they shouldn't have these issues.
Easy to test though:
1. Try sending with a byte order marker.
2. Try sending with <XXperson>
3. Try sending some characters with accents and the like.

Encrypting Java then Decrypting C# AES256 Encryption with HMACSHA256, Padding is invalid

I'm currently running into an issue where our decryption portion of our C# site is having trouble with the padding with the encrypted string from java. The .Net code throws this error "Padding is invalid and cannot be removed". The _signKey and _encKey are both 64 bytes.
public String encryptString(String plainText) {
byte[] ciphertext;
byte[] iv = new byte[16];
byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
String _signKey = "****************************************************************";
String _encKey = "****************************************************************";
try {
Mac sha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec shaKS = new SecretKeySpec(_signKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256.init(shaKS);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecureRandom randomSecureRandom = SecureRandom.getInstance("SHA1PRNG");
iv = new byte[cipher.getBlockSize()];
randomSecureRandom.nextBytes(iv);
IvParameterSpec ivParams = new IvParameterSpec(iv);
byte[] sessionKey = sha256.doFinal((_encKey + iv).getBytes(StandardCharsets.UTF_8));
// Perform Encryption
SecretKeySpec eks = new SecretKeySpec(sessionKey, "AES");
cipher.init(Cipher.ENCRYPT_MODE, eks, ivParams);
ciphertext = cipher.doFinal(plainBytes);
System.out.println("ciphertext= " + new String(ciphertext));
// Perform HMAC using SHA-256 on ciphertext
SecretKeySpec hks = new SecretKeySpec(_signKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(hks);
ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
outputStream2.write(iv);
outputStream2.write(ciphertext);
outputStream2.flush();
outputStream2.write(mac.doFinal(outputStream2.toByteArray()));
return Base64.encodeBase64String(outputStream2.toByteArray());
} catch (Exception e) {
e.printStackTrace();
}
return plainText;
}
Does does encrypt the string properly as far as I can tell. We cannot change any code on the .Net side to decrypt this because this is being used today.
public static string DecryptString(string ciphertext)
{
using (HMACSHA256 sha256 = new HMACSHA256(Encoding.UTF8.GetBytes(_signKey)))
{
// Convert message to bytes
byte[] encBytes = Convert.FromBase64String(ciphertext);
// Get arrays for comparing HMAC tags
byte[] sentTag = new byte[sha256.HashSize / 8];
byte[] calcTag = sha256.ComputeHash(encBytes, 0, (encBytes.Length - sentTag.Length));
// If message length is too small return null
if (encBytes.Length < sentTag.Length + _ivLength) { return null; }
// Copy tag from end of encrypted message
Array.Copy(encBytes, (encBytes.Length - sentTag.Length), sentTag, 0, sentTag.Length);
// Compare tags with constant time comparison, return null if no match
int compare = 0;
for (int i = 0; i < sentTag.Length; i++) { compare |= sentTag[i] ^ calcTag[i]; }
if (compare != 0) { return null; }
using (AesCryptoServiceProvider csp = new AesCryptoServiceProvider())
{
// Set parameters
csp.BlockSize = _blockBits;
csp.KeySize = _keyBits;
csp.Mode = CipherMode.CBC;
csp.Padding = PaddingMode.PKCS7;
// Copy init vector from message
var iv = new byte[_ivLength];
Array.Copy(encBytes, 0, iv, 0, iv.Length);
// Derive session key
byte[] sessionKey = sha256.ComputeHash(Encoding.UTF8.GetBytes(_encKey + iv));
// Decrypt message
using (ICryptoTransform decrypt = csp.CreateDecryptor(sessionKey, iv))
{
return Encoding.UTF8.GetString(decrypt.TransformFinalBlock(encBytes, iv.Length, encBytes.Length - iv.Length - sentTag.Length));
}
}
}
}
If there is anything that sticks out it would be appreciated for the reply.
I didn't read all your code, but this line in Java:
byte[] sessionKey = sha256.doFinal((_encKey + iv).getBytes(StandardCharsets.UTF_8));
does nothing useful or sensible. The "+" operator does string concatenation, but iv is a byte[], not a String. So java uses iv.toString(), which simply returns a String containing something like [B#1188e820 which is meaningless in this context.
Refer four java code and DotNet code:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //Java
csp.Padding = PaddingMode.PKCS7; //.Net
You are essentially using different padding, that is the probable source of error; however, there is an alternate view, Refer this great post and this for general fundamentals on padding
The cipher suites supported by deafult Oracle JVM implementation are here
If you notice it does not have 'AES/CBC/PKCS7Padding', a PKCS#7 padding implementation is available in sun.security package, refer this, otherwise you could use Bouncy Castle packages. It would be recommendable to use Bouncy Castle as com.sun package are generally considered unsupported.

how to decrypt in java (snippet of my code included)

I am wanting to create a functional Java chat application.
So I have a small application which allows users to connect via server classes and talk with each other via client classes and I have started to add Encryption. I am having trouble decrypting output from other clients in my Java chat application.
can someone help me please?
snippet of my code is included below:
THE CLIENTGUI.JAVA CLASS (encrypt is a button which is clicked)
if(o == encrypt) {
String change = null;
try{
change = tf.getText();
change = FileEncryption.encryptString(change);
tf.setText("" + change);
return;
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
finally{
}
THE FILEENCRYPTION.JAVA
public class FileEncryption {
//Initial Vector
public static final byte[] iv = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
//EncryptAndDecrypt String -> Input : PlainText + Return : CipherText+DecipherText
public static String encryptString(String src) throws Exception
{
String dst="";
//Not Input!
if(src == null || src.length()==0)
return "";
//Encryption Setting
byte[] k="Multimediaproces".getBytes();
SecretKeySpec Key = new SecretKeySpec(k,"AES");
IvParameterSpec ivspec = new IvParameterSpec(iv);
Cipher encryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptCipher.init(Cipher.ENCRYPT_MODE,Key,ivspec);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
CipherOutputStream cout = new CipherOutputStream(baos,encryptCipher);
cout.write(src.getBytes());
cout.flush(); //ByteOutputStream -> Write Encryption Text
cout.close();
// in encrypt method
dst = DatatypeConverter.printHexBinary(baos.toByteArray());
return dst;
}
//String src -> EncryptedData
public static String decryptString(String src) throws Exception
{
//src value is Encrypted Value!
//So, src value -> Not Byte!
String dst="";
byte[] encryptedBytes = DatatypeConverter.parseHexBinary(src);;
//Not Input!
if(src == null || src.length()==0)
return "";
//Decryption Setting
IvParameterSpec ivspec = new IvParameterSpec(iv);
byte[] k="Multimediaproces".getBytes();
SecretKeySpec Key = new SecretKeySpec(k,"AES");
Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
decryptCipher.init(Cipher.DECRYPT_MODE,Key,ivspec);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBytes);
CipherInputStream cin = new CipherInputStream(bais,decryptCipher);
byte[] buf = new byte[1024];
int read;
while((read=cin.read(buf))>=0) //reading encrypted data!
{
baos.write(buf,0,read); //writing decrypted data!
}
// closing streams
cin.close();
dst = new String(baos.toByteArray());
return dst;
}
}
the problem is that when i try to decrypt the code entering the following code:
if(o == decrypt) {
try{
msg = tf.getText();
msg = FileEncryption.decryptString(msg);
fop.
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}finally{
}
Currently, it ALLOWS me to encrypt what I type into text field.
It does not allow me to decrypt the output of what the users have said in the chat. The current code I have included for the decrypt does not function.
Can anyone help me? or have any suggestions that I could make to my program to help it decrypt?
Thanks
EDIT:
Your best bet would probably be to simply use SSL sockets for your network communications, rather than writing the encryption code yourself. While your question isn't exactly a duplicate of this one, you'd likely be well served by the answers here:
Secret Key SSL Socket connections in Java
I suspect that the problem is not passing the encrypted status between the 2 clients.
If the "encrypt" object is a button then it is a button on only one side of the client-client connection. You will need to pass the encrypted state to the other client, so that it knows to decrypt the message.
A short cut to confirming this would be to automatically show the plaintext and decrypted message on the receiving end. One of them will always be gibberish but it should change depending on the use of the encrypt button.
Good luck :)

Encrypted Data with DES - JAVA

Given the following example:
String f="A000000000000000";
FileInputStream fis = new FileInputStream("C:\\Users\\original.txt");
byte[] bytes = DatatypeConverter.parseHexBinary(f);
SecretKey key = new SecretKeySpec(bytes, 0, bytes.length, "DES");
String strDataToEncrypt = new String();
String strCipherText = new String();
String strDecryptedText = new String();
try{
Cipher desCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
desCipher.init(Cipher.ENCRYPT_MODE,key);
//read from file and transform to String
try{
builder = new StringBuilder();
int ch;
while((ch = fis.read()) != -1){
builder.append((char)ch);
}
}catch (IOException e){
}
byte[] byteDataToEncrypt = builder.toString().getBytes();
byte[] byteCipherText = desCipher.doFinal(byteDataToEncrypt);
strCipherText = new BASE64Encoder().encode(byteCipherText);
System.out.println(strCipherText);
the encrypted data is different everytime I compile with the same key value i , i tried different codes and the encrypted data was always the same , what's wrong here ?
The documentation for javax.crypto.Cipher.init says, in part:
If this cipher requires any algorithm parameters that cannot be
derived from the given key, the underlying cipher implementation is
supposed to generate the required parameters itself (using
provider-specific default or random values)
DES CBC (Cipher Block Chaining) mode requires an Initialization Vector (IV). If you do not provide one (and you should not, because it opens you up to dictionary attacks), a random one will be generated.
But if you want the encrypted data to be the same every time, you need to specify the IV using an IvParameterSpec:
byte[] iv = DatatypeConverter.parseHexBinary("0000000000000000");
IvParameterSpec ips = new IvParameterSpec(iv);
desCipher.init(Cipher.ENCRYPT_MODE, key, ips);
If you do let it generate a random IV, you can retrieve the generated IV with desCipher.getIV().

A string encrypted (AES) after decryption prints the same value , but false on equals()

My program sends a string encrypted (AES) with received session key to Client to prove the key is correct.
Client should decrypt it, get the string and verify it with original one.
Program works fine. It encrypts and decrypts the string. It prints the string I need, but gives me false when I do String.equals(string).
I can figure out why.
There is the encryption part of my code:
// ----create a challenge for Client (to check if the session key is correct)--------
public void sessionKeyVer(String challenge, File out) throws Exception{
aesCipher.init(Cipher.ENCRYPT_MODE, aeskeySpec); // switching mode for encryption
CipherOutputStream os = new CipherOutputStream(new FileOutputStream(out), aesCipher); //output stream to another file
os.write(challenge.getBytes("UTF-8"));// function to copy String to outputstream
os.close(); //close the stream
}
There is the decryption part:
public boolean sessionKeyVer(File file) throws Exception{
aesCipher.init(Cipher.DECRYPT_MODE, aeskeySpec); // switching mode for decryption
CipherInputStream is = new CipherInputStream(new FileInputStream(file), aesCipher); //output stream to another file
ByteArrayOutputStream os = new ByteArrayOutputStream();
int i;
byte[] b = new byte[1024];
while((i=is.read(b))!=-1) {
os.write(b, 0, i);
}
is.close();
os.close();
String file_string = new String(b,"UTF-8");
System.out.print(file_string);
return file_string.equals(challenge); //return false
}
Thank you.
The first part is the encryption part. The second part is the decryption part.
The second part is wrong. You are decrypting the last part of the still-encrypted buffer, rather than the entire, decrypted ByteArrayOutputStream, and committing a size error in the process too.
String file_string = new String(b,"UTF-8");
should be
String file_string = new String(os.toByteArray(), "UTF-8");

Categories