Stashh uses AES-256-GCM
to encrypt and decrypt content with a symmetric key. This key is then embedded into the private metadata of an NFT.
On-Platform Encryption & Decryption
Private Assets
When you upload a private asset to Stashh, the platform automatically encrypts it. Stashh then embeds the decryption key into the asset's metadata. This ensures that only the asset owner, when accessing their content on Stashh, can seamlessly decrypt and view the asset. For the asset owner, this entire process is transparent, providing a fluid and secure user experience.
Public Assets
Public assets are different; they are uploaded in their original form without encryption. This approach ensures that these assets are openly accessible to all users without the need for decryption.
Off-Platform Encryption & Decryption
For those assets that are encrypted outside of Stashh, you can still enjoy a hassle-free experience within the platform. The only requirement is that these external assets should be encrypted using the AES-256-GCM
method. Furthermore, the decryption key must be embedded correctly within the MediaFile Extension.
{
"file_type": "...",
"extension": "...",
"authentication": {
"key": "KEY_GOES_HERE"
},
"url": "..."
}
It's essential to structure the encrypted payload as described in the Payload Structure section for Stashh to recognize, decrypt, and display the content seamlessly. Once these conditions are met, Stashh will automatically detect the embedded key and decrypt the content for viewing.
Payload Structure
To ensure the encrypted content is compatible with Stashh it's vital to structure the encrypted payload in a specific format.
- Initialization Vector Length (IV_LENGTH): The very first byte of the encrypted content indicates the length of the Initialization Vector (IV). In most cases this value will be
12
. This byte helps identify the length of the subsequent IV. - Initialization Vector (IV): Directly after the
IV_LENGTH
byte, the content includes the Initialization Vector itself. It spans a number of bytes equivalent to the value specified in theIV_LENGTH
. - Authentication Tag: After the IV, the next 16 bytes represent the authentication tag generated during encryption using AES-256-GCM mode. This is crucial for ensuring data integrity and authenticity.
- Encrypted Ciphertext: The bytes following the authentication tag are the actual encrypted content or the ciphertext.
So, when constructing an encrypted payload for Stashh, it's structured as:
[IV_LENGTH][IV...][Authentication Tag...][Ciphertext...]
This sequence ensures that Stashh can determine the Initialization Vector's size, extract the IV, verify the authenticity of the data using the authentication tag, and subsequently decrypt the ciphertext. Adhering to this structure will enable seamless decryption on the platform.
Reference Implementations
Overview
- Initialization Vector (IV): AES-GCM requires an Initialization Vector (IV) to ensure that each encryption is unique. These implementations use a 12-byte IV, which is randomly generated for each encryption operation.
- Tag: AES-GCM also generates an authentication tag that verifies the integrity of the data during decryption. In this implementation, the tag is 16 bytes (128-bit).
- Key: A symmetric key is used for both encryption and decryption processes.
TypeScript Implementation
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12;
export const encrypt = (dataBuffer: Buffer, key: string): Buffer => {
const iv = crypto.randomBytes(IV_LENGTH);
const cipherKey = Buffer.from(key);
const cipher = crypto.createCipheriv(ALGORITHM, cipherKey, iv);
const encryptedBuffer = Buffer.concat([
cipher.update(dataBuffer),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
let bufferLength = Buffer.alloc(1);
bufferLength.writeUInt8(iv.length, 0);
return Buffer.concat([bufferLength, iv, authTag, encryptedBuffer]);
};
export const decrypt = (dataBuffer: Buffer, key: string): Buffer => {
const cipherKey = Buffer.from(key);
const ivSize = dataBuffer.readUInt8(0);
const iv = dataBuffer.slice(1, ivSize + 1);
const authTag = dataBuffer.slice(ivSize + 1, ivSize + 17);
// Create decipher
const decipher = crypto.createDecipheriv(ALGORITHM, cipherKey, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([
decipher.update(dataBuffer.slice(ivSize + 17)),
decipher.final(),
]);
};
encrypt
: This method accepts a data buffer and a key as inputs and outputs the encrypted content structured as:- 1 byte indicating the IV length.
- The IV itself.
- The GCM authentication tag.
- The encrypted content.
decrypt
: This function takes an encrypted buffer and a key as inputs, and returns the decrypted content. The buffer is expected to contain:- The IV length in its first byte.
- The IV itself, determined by the previously read length.
- The GCM authentication tag.
- The actual encrypted content.
The function starts by creating a random IV. It then initializes the AES-GCM cipher for encryption, using the provided key and generated IV. After encrypting the data, the function assembles the IV, authentication tag, and encrypted content into a single buffer for output.
The method initializes the AES-GCM cipher for decryption, using the provided key and the extracted IV. After decrypting the data and verifying it against the authentication tag, the decrypted content is returned.
C# Reference Implementation
using System.Security.Cryptography;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
public static class EncryptionService
{
private const int IvLengthBytes = 12;
private const int TagLengthBytes = 16;
public static async Task<byte[]> Encrypt(byte[] plaintextBytes, byte[] key)
{
var iv = new byte[IvLengthBytes];
RandomNumberGenerator.Fill(iv);
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), TagLengthBytes * 8, iv);
cipher.Init(true, parameters);
var cipherOutput = new byte[plaintextBytes.Length];
var finalBlock = new byte[cipher.GetBlockSize() * 2];
var tag = new byte[TagLengthBytes];
var offset = cipher.ProcessBytes(plaintextBytes, 0, plaintextBytes.Length, cipherOutput, 0);
var finalBlockLength = cipher.DoFinal(finalBlock, 0);
Buffer.BlockCopy(finalBlock, 0, cipherOutput, offset, finalBlockLength - TagLengthBytes);
Buffer.BlockCopy(finalBlock, finalBlockLength - TagLengthBytes, tag, 0, TagLengthBytes);
await using var output = new MemoryStream();
await output.WriteAsync(new byte[1] { Convert.ToByte(iv.Length) });
await output.WriteAsync(iv);
await output.WriteAsync(tag);
await output.WriteAsync(cipherOutput);
await output.FlushAsync();
return output.ToArray();
}
public static byte[] Decrypt(byte[] cipherTextBytes, byte[] key)
{
using var cipherTextStream = new MemoryStream(cipherTextBytes);
using var cipherTextReader = new BinaryReader(cipherTextStream);
var ivLength = Convert.ToInt32(cipherTextReader.ReadByte());
var iv = cipherTextReader.ReadBytes(ivLength);
var tag = cipherTextReader.ReadBytes(TagLengthBytes);
var content = cipherTextReader.ReadBytes(cipherTextBytes.Length - 1 - iv.Length - TagLengthBytes);
var cipherTextAndTag = content.Concat(tag).ToArray();
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), TagLengthBytes * 8, iv);
cipher.Init(false, parameters);
var plaintextBytes = new byte[cipher.GetOutputSize(cipherTextAndTag.Length)];
var offset = cipher.ProcessBytes(cipherTextAndTag, 0, cipherTextAndTag.Length, plaintextBytes, 0);
cipher.DoFinal(plaintextBytes, offset);
return plaintextBytes;
}
}
This implementation relies on the BouncyCastle.DotNetCore package, which you can add to your solution with the following command:
dotnet add package BouncyCastle.NetCore --version 1.9.0
Encrypt
: This method takes plaintext bytes and a key as inputs and returns the encrypted content in the following order:- 1 byte indicating the IV length.
- The IV itself.
- The GCM authentication tag.
- The encrypted content.
Decrypt
: This method takes the encrypted bytes and a key as input and returns the decrypted content.- The IV length is read from the first byte.
- The IV is then read based on the previously acquired length.
- The GCM authentication tag is read.
- The remaining bytes are considered the actual encrypted content.
The method begins by generating a random IV. The AES-GCM mode is then initialized for encryption using the BouncyCastle GcmBlockCipher
with the AesEngine
. The encrypted content and authentication tag are computed and combined with the IV into a single byte array for output.
The AES-GCM mode is initialized for decryption, and the content is decrypted and verified against the authentication tag. If the tag doesn't match, a cryptographic exception will be thrown by the library, indicating the data may have been tampered with.