Storing service account credentials securely in clickonce application - c#

I'm writing a ClickOnce application that runs a batch file process with service account credentials. I need to store the service account credentials so that the program can add the username/password to the process.startinfo property before running the process. The users do not know this password, so there's no prompt for them to enter in a password. I believe this means I cannot store the hash and verify the password that way, the hash value I generate must be reversible so that it can add the correct password to the startinfo property. I searched around this site and came up with a Frankenstein-type solution that works, but it's not very secure. Currently, I used this method to encrypt the password, stored the encrypted value, then use the decrypt method to obtain the password during runtime (the encrypt method is never ran during runtime, I ran it in Visual Studio during debug, copied the value, then used that value in the decrypt method below this):
// used to generate decrypted acct creds
private void EncryptText(string plaintext)
{
string outsrt = null;
RijndaelManaged aesAlg = null;
try
{
// generate key from secret and salt
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(sharedsecret, _salt);
aesAlg = new RijndaelManaged();
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream mEncrypt = new MemoryStream())
{
// prepend the IV
mEncrypt.Write(BitConverter.GetBytes(aesAlg.IV.Length), 0, sizeof(int));
mEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length);
using (CryptoStream csEncrypt = new CryptoStream(mEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
// write all data to the stream
swEncrypt.Write(plaintext);
}
}
outsrt = Convert.ToBase64String(mEncrypt.ToArray());
}
}
finally
{
if (aesAlg != null)
aesAlg.Clear();
}
Console.WriteLine(outsrt);
}
Here's the decrypt method:
private string GetServiceAcctPW()
{
// Declare the RijndaelManaged object
// used to decrypt the data.
RijndaelManaged aesAlg = null;
// Declare the string used to hold
// the decrypted text.
string plaintext = null;
try
{
// generate the key from the shared secret and the salt
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(sharedsecret, _salt);
// Create the streams used for decryption.
byte[] bytes = Convert.FromBase64String("EncryptedValueHere");
using (MemoryStream msDecrypt = new MemoryStream(bytes))
{
// Create a RijndaelManaged object
// with the specified key and IV.
aesAlg = new RijndaelManaged();
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
// Get the initialization vector from the encrypted stream
aesAlg.IV = ReadByteArray(msDecrypt);
// Create a decrytor to perform the stream transform.
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
// Read the decrypted bytes from the decrypting stream
// and place them in a string.
plaintext = srDecrypt.ReadToEnd();
}
}
}
catch(Exception e)
{
Console.WriteLine("Error decrypting password");
Console.WriteLine(e.StackTrace);
logger.WriteToLog(Logger.LogCodes.ERROR, "Error decrypting service account password");
MessageBox.Show("An error occurred while trying to start the installation process\nPlease contact the Service Desk for further assistance");
}
finally
{
// Clear the RijndaelManaged object.
if (aesAlg != null)
aesAlg.Clear();
}
return plaintext;
}
This code works just fine, however, I know it's not secure. My code review guy said he was able to crack it with dotPeek in an hour because it's only adding a layer of obfuscation. What would be the best/proper way to store these credentials within the application?

The encryption key is on a dedicated server.
The password is sent to the server along with an id to be encrypted and the encrypted password returned for DB storage.
When the the password is needed a request is made to the dedicated server with the id and a decrypted password is returned.
The password is never saved to disk and the key is never available off the dedicated server.
The dedicated server is kind-of-like a poor-mans HSM.
This is encryption, not hashing. The encryption key is secret along with a random IV that that is saved with the id on the dedicated server. The key is not available and not related to the password so there is no better attack than brute force against the encryption key which is essentially to large to be attacked by brute force.
The server needs to be very secure, only a couple of two factor logins and not available to the Internet.

Related

AES-256-CBC in .NET Core (C#)

I am searching for C# Code to reproduce the following openssl command.
openssl enc -d -aes-256-cbc -in my_encrypted_file.csv.enc -out my_decrypted_file.csv -pass file:key.bin
Additional information:
The encrypted file in present as byte[]
The key.bin is a byte[] with length of 256 (the key is obtained by a more simple decryption of yet another file, which i managed to realize in C#).
I have been trying out various examples found by searching the web.
The problem is, that all of these examples require an IV (initialization vector). Unfortunately, I don't have an IV and no one on the team knows what this is or how it could be defined.
The openssl command does not seem to need one, so I am a bit confused about this.
Currently, the code, I am trying with, looks as follows:
public static string DecryptAesCbc(byte[] cipheredData, byte[] key)
{
string decrypted;
System.Security.Cryptography.Aes aes = System.Security.Cryptography.Aes.Create();
aes.KeySize = 256;
aes.Key = key;
byte[] iv = new byte[aes.BlockSize / 8];
aes.IV = iv;
aes.Mode = CipherMode.CBC;
ICryptoTransform decipher = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream(cipheredData))
{
using (CryptoStream cs = new CryptoStream(ms, decipher, CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs))
{
decrypted = sr.ReadToEnd();
}
}
return decrypted;
}
}
The code fails saying that my byte[256] key has the wrong length for this kind of algorithm.
Thanks for any help with this!
Cheers, Mike
The posted OpenSSL statement uses the -pass file: option and thus a passphrase (which is read from a file), see openssl enc. This causes the encryption process to first generate a random 8 bytes salt and then, together with the passphrase, derive a 32 bytes key and 16 bytes IV using the (not very secure) proprietary OpenSSL function EVP_BytesToKey. This function uses several parameters, e.g. a digest and an iteration count. The default digest for key derivation is MD5 and the iteration count is 1. Note that OpenSSL version 1.1.0 and later uses SHA256 as default digest, i.e. depending on the OpenSSL version used to generate the ciphertext, the appropriate digest must be used for decryption. Preceding the ciphertext is a block whose first 8 bytes is the ASCII encoding of Salted__, followed by the 8 bytes salt.
Therefore, the decryption must first determine the salt. Based on the salt, together with the passphrase, key and IV must be derived and then the rest of the encrypted data can be decrypted. Thus, first of all an implementation of EVP_BytesToKey in C# is required, e.g. here. Then a possible implementation could be (using MD5 as digest):
public static string DecryptAesCbc(byte[] cipheredData, string passphrase)
{
string decrypted = null;
using (MemoryStream ms = new MemoryStream(cipheredData))
{
// Get salt
byte[] salt = new byte[8];
ms.Seek(8, SeekOrigin.Begin);
ms.Read(salt, 0, 8);
// Derive key and IV
OpenSslCompat.OpenSslCompatDeriveBytes db = new OpenSslCompat.OpenSslCompatDeriveBytes(passphrase, salt, "MD5", 1);
byte[] key = db.GetBytes(32);
byte[] iv = db.GetBytes(16);
using (Aes aes = Aes.Create())
{
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
aes.Key = key;
aes.IV = iv;
// Decrypt
ICryptoTransform decipher = aes.CreateDecryptor(aes.Key, aes.IV);
using (CryptoStream cs = new CryptoStream(ms, decipher, CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs, Encoding.UTF8))
{
decrypted = sr.ReadToEnd();
}
}
}
}
return decrypted;
}
Note that the 2nd parameter of DecryptAesCbc is the passphrase (as string) and not the key (as byte[]). Also note that StreamReader uses an encoding (UTF-8 by default), which requires compatible data (i.e. text data, but this should be met for csv files). Otherwise (i.e. for binary data as opposed to text data) StreamReader must not be used.

Different encryption is created for the same value

I have used below code to encrypt my value. However, I noticed that for the same value new encryption format is generated instead of same encryption value. Can anyone help me to solve this issue?
Example:
Value is HelloWorld123$
When I executed for the first time, I am getting this encryption - EAAAAE+WzLTCsNOJSQBuTwnRsfrRxqLa6WLVr0zWQ8eozkr1
When I executed for the second time, I am getting this encryption - EAAAAEJuBne0limVQ4aQij89v2SjU8eHasyDlnsGGQ1MD43V
Question: How can I solve to get same encryption all time for same value?
private static byte[] _salt = { 1, 2, 3, 4, 5, 6, 7, 8 }; // Array of numbers
internal static byte[] key = { 0x0A, 01, 02, 0x48 };
/// <summary>
/// Encrypt the given string using AES. The string can be decrypted using
/// DecryptStringAES(). The sharedSecret parameters must match.
/// </summary>
/// <param name="plainText">The text to encrypt.</param>
/// <param name="sharedSecret">A password used to generate a key for encryption.</param>
private static string EncryptStringAES(string plainText, string sharedSecret)
{
if (string.IsNullOrEmpty(plainText))
throw new ArgumentNullException("plainText");
//if (string.IsNullOrEmpty(sharedSecret))
// throw new ArgumentNullException("sharedSecret");
string outStr = null; // Encrypted string to return
RijndaelManaged aesAlg = null; // RijndaelManaged object used to encrypt the data.
try
{
// generate the key from the shared secret and the salt
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(sharedSecret, _salt);
// Create a RijndaelManaged object
aesAlg = new RijndaelManaged();
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
// Create a decryptor to perform the stream transform.
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
// prepend the IV
msEncrypt.Write(BitConverter.GetBytes(aesAlg.IV.Length), 0, sizeof(int));
msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length);
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
//Write all data to the stream.
swEncrypt.Write(plainText);
}
}
outStr = Convert.ToBase64String(msEncrypt.ToArray());
}
}
finally
{
// Clear the RijndaelManaged object.
if (aesAlg != null)
aesAlg.Clear();
}
// Return the encrypted bytes from the memory stream.
return outStr;
}
/// <summary>
/// Decrypt the given string. Assumes the string was encrypted using
/// EncryptStringAES(), using an identical sharedSecret.
/// </summary>
/// <param name="cipherText">The text to decrypt.</param>
/// <param name="sharedSecret">A password used to generate a key for decryption.</param>
private static string DecryptStringAES(string cipherText, string sharedSecret)
{
if (string.IsNullOrEmpty(cipherText))
throw new ArgumentNullException("cipherText");
//if (string.IsNullOrEmpty(sharedSecret))
// throw new ArgumentNullException("sharedSecret");
// Declare the RijndaelManaged object
// used to decrypt the data.
RijndaelManaged aesAlg = null;
// Declare the string used to hold
// the decrypted text.
string plaintext = null;
try
{
// generate the key from the shared secret and the salt
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(sharedSecret, _salt);
// Create the streams used for decryption.
byte[] bytes = Convert.FromBase64String(cipherText);
using (MemoryStream msDecrypt = new MemoryStream(bytes))
{
// Create a RijndaelManaged object
// with the specified key and IV.
aesAlg = new RijndaelManaged();
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
// Get the initialization vector from the encrypted stream
aesAlg.IV = ReadByteArray(msDecrypt);
// Create a decrytor to perform the stream transform.
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
// Read the decrypted bytes from the decrypting stream
// and place them in a string.
plaintext = srDecrypt.ReadToEnd();
}
}
}
finally
{
// Clear the RijndaelManaged object.
if (aesAlg != null)
aesAlg.Clear();
}
return plaintext;
}
private static byte[] ReadByteArray(Stream s)
{
byte[] rawLength = new byte[sizeof(int)];
if (s.Read(rawLength, 0, rawLength.Length) != rawLength.Length)
{
throw new SystemException("Stream did not contain properly formatted byte array");
}
byte[] buffer = new byte[BitConverter.ToInt32(rawLength, 0)];
if (s.Read(buffer, 0, buffer.Length) != buffer.Length)
{
throw new SystemException("Did not read byte array properly");
}
return buffer;
}
User Case:
I have a form which insert form value into database. Including some valuable items which are encrypted.
I have another form which checks whether value exist in database, When I using some method for some lookup functions I need to compare many condition for the same value. So I am directly coparing encrypted values. But as new value is created. I am unable to compare those value
Hope I am able to explain mu Use Case
Take a look at - https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes?view=netcore-3.1
This class uses a pseudorandom number generator in its work, which means that it is supposed to generate different data over time which means that your encrypted data is supposed to change. It always decrypts the same, but it isn't intended to be deterministic (producing same output for same inputs)
If you're doing something like storing a password you should use a hashing function (eg SHA256) rather than an encrypting function; they produce the same output for given inputs so you can compare the output today to the output yesterday and if they're the same you can decide that the user typed the same password today as he did yesterday
Ultimately you are probably in (or should want to be in) one of two places:
you need to store some data securely and be able to retrieve it and turn it back into the data it was, maybe because you're the only person who knows it but it needs to be used or known elsewhere. You need to encrypt the data for storage, decrypt it, use it, if you update it you need to re-encrypt and store it again
you need to be able to confirm some data that someone else knows; they will give you the data and you will check your record and decide whether they got it right or not. You need to hash the data then forget the original, the next time the person appears claiming they know the original data you hash what they claim it is and compare the hashes. If it's the same then their claim they know the data is correct
You might be wanting to have an encryption that is deterministic, but it's quite a rare thing to want and it feels more like you're misunderstanding some aspect of your use case. Go into more detail so we can better advise
If you encrypt the same value it will give you different result, it’s basically security and most of the encryptions work in this manner.
If it gives you same results on each encryption then it will be deterministic and should be avoided.

Key generation for symmetric algorithm

I have simple symmetric encrypt/decrypt application that works fine:
namespace Crypto
{
class Program
{
public static void EncryptSomeText()
{
string original ="My secretdata!";
using (SymmetricAlgorithm symmetricAlgorithm =
new AesManaged())
{
byte[] encrypted = Encrypt(symmetricAlgorithm, original);
string roundtrip = Decrypt(symmetricAlgorithm, encrypted);
// Displays: My secret data! 
Console.WriteLine("Original:{ 0}", original);
Console.WriteLine("RoundTrip:{ 0}", roundtrip);
}
}
static byte[] Encrypt(SymmetricAlgorithm aesAlg, string plainText)
{
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt =
new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
}
return msEncrypt.ToArray();
}
}
}
static string Decrypt(SymmetricAlgorithm aesAlg, byte[] cipherText)
{
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt =
new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
}
static void Main(string[] args)
{
SymmetricAlgorithm sma = SymmetricAlgorithm.Create();
byte[] b = Encrypt(sma,"bla bla");
string s= Decrypt(sma, b);
Console.WriteLine("Decrypted {0}", s);
}
}
}
I'm just wondering regarding key creation. Does it means that it is created in automatic way during symmetric algorithm creation? Is it generated each time different? I suppose, user should pass key for this purpose.
SymmetricAlgorithm sma = SymmetricAlgorithm.Create();
Random key is generated when you access Key property of your SymmetricAlgorithm, assuming you did not set that Key before that to some predefined key. It's not generated during construction of SymmetricAlgorithm itself.
var sma = SymmetricAlgorithm.Create();
// no key generated here yet
var key = sma.Key; // generated key
// accessing `Key` causes random key generation
var sma = SymmetricAlgorithm.Create();
// no key here
sma.Key = myKey;
// you set the key, so no random key is generated
The same is true for IV (nonce) value.
Generating random key makes sense only once, then you have to share it between encrypting\decrypting party in some safe way and use it for future encryptions\decryptions. While you are there, note that IV value should be random for each encryption, so usually you just prepend that IV value to the encrypted binary array and before decrypting you cut that IV from the beginning of encrypted array and use for decryption.
On this github page there is utility class written in Java which performs encryption/decryption using symmetric algorithm, below could be one of encryption flows using symmetric algorithm :
generate secret key using the selected algorithm (DES, 3DES, AES etc)
generate secure random number of seed bytes which is computed with available the seed generation algorithm
probably text message to be encrypted is not the multiples of 8 byte blocks that's why message must be padded with additional bytes to make the text message to be multiples of 8-byte blocks.(e.g. PKCS5Padding padding scheme )
use random generated secure seed bytes as initialization vector as block cipher
initialize cipher function with the symmetric key and block cipher
finish encryption
apply binary to text encoding with selected standard for initialization vector(IV) and encrypted binary data
define message format before sending in order to split the message into IV and encrypted for decryption on recipient side

in what format/file to store application data

I have a desktop application developed in c# language that has some games that the users play and the App will store their names, scores and other information on the client computer, currently I am using an xml file to store the info of the user, but a drawback is that the user can edit the file manually and mess with it by editing their scores or name.
I want to store the info so that the users can not manually edit it, so what file format should I use or should I encrypt the data before storing in the file.
My file is stored in the C:\Users\jobs\AppData\Local folder
tnx
If you need some basic protection against an average PC user you probably want to use something really simple, like this:
// data for example
var data = new XElement("gamedata",
new XElement("player", new XAttribute("name", "t0taln00b"),
new XElement("score",
new XAttribute("game", "bite your elbow"),
new XAttribute("score", 9000),
new XAttribute("progress", "19 %"))
)
);
// set up encryption.
// You probably will want to do this once at program startup and store Key and IV globally
var rnd = new Random(12562);
var keysize = 128;
byte[]
Key = new byte[keysize / 8],
IV = new byte[keysize / 8];
rnd.NextBytes(Key);
rnd.NextBytes(IV);
// lets encrypt
using (Aes aes = new AesManaged() { Padding = PaddingMode.PKCS7, KeySize = keysize })
{
aes.Key = Key;
aes.IV = IV;
using (Stream file = new FileStream("save.xml.aes", FileMode.Create, FileAccess.Write))
using (Stream encrypter = new CryptoStream(file, aes.CreateEncryptor(), CryptoStreamMode.Write))
data.Save(encrypter);
}
//and decrypt
using (Aes aes = new AesManaged() { Padding = PaddingMode.PKCS7, KeySize = keysize })
{
aes.Key = Key;
aes.IV = IV;
using (Stream file = new FileStream("save.xml.aes", FileMode.Open, FileAccess.Read))
using (Stream decrypter = new CryptoStream(file, aes.CreateDecryptor(), CryptoStreamMode.Read))
{
var loaded = XElement.Load(decrypter);
Console.WriteLine(loaded.ToString());
}
}
Please note that this would be an extremely weak encryption. Keys generated by System.Random cannot be strong, and a whole encryption code can be revealed with tools like .NET Reflector.
If you need really strong encryption i would suggest having something like a webserver which encrypts the file with an asymmetric encryption and keeps the private key secure. This will not absolutely prevent a hacker from reading the file, but at least will make you totally sure that he cannot modify its content.

Why do I get PKCS7 padding errors when using CryptoStream?

I'm trying to encrypt a stream and decrypt it again. When encrypting, I store the salt and IV (8 and 16 bytes) first into the target stream. When decrypting, I get a padding error in the line where CopyTo() is called. The full source of the class can be found in a Gist.
The relevant code snippet for encryption is:
// Set position to start of stream.
encryptedOutStream.Seek (0, SeekOrigin.Begin);
// Store the salt in the output stream. The salt is not a secret. Salt is used to generate different keys for identical passwords.
var keyInfo = GenerateKey (password);
encryptedOutStream.Write (keyInfo.Salt, 0, keyInfo.Salt.Length);
// Store the IV in the output stream. The IV is randomly generated if not set explicitly. It is not a secret and used to create
// different encrypted output for identical plaintext input when using CBC cipher mode.
encryptedOutStream.Write (aesAlgo.IV, 0, aesAlgo.IV.Length);
// Let the algorithm know our key.
aesAlgo.Key = keyInfo.Key;
// Get an encrypting ICryptoTransform interface from the algorithm.
using(var cryptoTransform = aesAlgo.CreateEncryptor ())
// Pump the input stream through a crypto stream wrapping a memory stream.
using(var encryptionStream = new CryptoStream(encryptedOutStream, cryptoTransform, CryptoStreamMode.Write))
{
plainInStream.CopyTo (encryptionStream);
}
and for decryption:
// Read the salt.
byte[] salt = new byte[8];
encryptedInStream.Read (salt, 0, 8);
// Read the IV.
byte[] iv = new byte[16];
encryptedInStream.Read (iv, 0, 16);
aesAlgo.IV = iv;
// Generate the key from the password and the salt.
var keyInfo = GenerateKey (password, salt);
aesAlgo.Key = keyInfo.Key;
// Get a decrypting ICryptoTransform interface from the algorithm.
using(var cryptoTransform = aesAlgo.CreateDecryptor ())
// Pump the input stream through a crypto stream wrapping a memory stream.
using(var decryptionStream = new CryptoStream(encryptedInStream, cryptoTransform, CryptoStreamMode.Read))
{
decryptionStream.CopyTo (decryptedOutStream);
}
I suspect an issue involving the EncryptString and DecryptString methods, specifically the lines:
encryptedString = Encoding.UTF8.GetString(encryptedOutStream.ToArray());
and
using (var encryptedInStream = new MemoryStream(Encoding.UTF8.GetBytes(s)))
Effectively, this code incorrectly attempts to use text encoding on ciphertext, which is binary data. This will introduce errors when the binary data does not happen to be a legal UTF8 sequence, corrupting the ciphertext and introducing padding issues. Instead, a binary encoding method needs to be used (most simply, base64).
To correct the issue, change the above lines to:
// change line 281:
// encryptedString = Encoding.UTF8.GetString(encryptedOutStream.ToArray());
// to:
encryptedString = Convert.ToBase64String(encryptedOutStream.ToArray());
// change line 251:
// using (var encryptedInStream = new MemoryStream(Encoding.UTF8.GetBytes(s)))
// to:
using (var encryptedInStream = new MemoryStream(Convert.FromBase64String(s)))
With this change, the conversion appears to work. A simple driver to encrypt and then decrypt the plaintext "payload" with the password "password" prints the desired output:
string password = "password";
SymmetricCrypto c = new SymmetricCrypto();
string ct = c.EncryptString("payload", password);
Console.WriteLine(ct); // prints sLSZfzVQGCoML29... (ciphertext will vary)
string dt = c.DecryptString(ct, password);
Console.WriteLine(dt); // prints "payload"

Categories