How to Encrypt and Decrypt Data
PrerequisiteYou must generate a 256-bit elliptic curve keypair (EC-256), also known as NIST P-256, secp256r1, or prime256v1, and share the public key with Blinc.
The private key must be generated and securely stored in PKCS#8 format within your environment. Never share the private key with Blinc.
We recommend using separate keypairs for test and live/production environments.
For a Blinc request that requires encryption, encrypt the plaintext JSON payload before transmission. Base64-encode the encrypted payload envelope and send it in the data field of the request body.
--data '{"data":"<base64 encrypted payload envelope>"}'Use Base64-encoded X.509 SubjectPublicKeyInfo (SPKI) DER public keys, without PEM header or footer lines. The sender also creates a fresh, single-use P-256 key pair for each encrypted request. This is separate from the long-term key pair above.
// Sample public key
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3G+eSLh2rQ6zxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxdQpCOOJw==Encrypt a request to Blinc
Use this flow only when calling an E2EE-enabled Blinc endpoint. Encrypt the request body with Blinc's public key. Do not use the public key you shared with Blinc.
- Import Blinc's public key from Base64.
- Generate a new P-256 key pair for this request.
- Create a shared AES key using HKDF-SHA256 with info
blinc-e2ee. - Convert the request body to UTF-8 bytes, create a random 12-byte nonce, and encrypt it with AES-256-GCM.
- Export the new public key as
0x04 || X || Y. - Join the new public key, nonce, ciphertext, and tag together, then Base64-encode the result to get the payload envelope.
single-use public key (65 bytes)
nonce (12 bytes)
ciphertext (N bytes)
GCM tag (16 bytes)Encrypt code examples
Each example takes the plaintext request body and Blinc's Base64 public key, then returns the Base64 envelope to send as the encrypted request body.
using System;
using System.Security.Cryptography;
using System.Text;
public static string Envelope(string plaintext, string publicKeyBase64)
{
// 1. Import public Key
using var publicKey = ECDiffieHellman.Create();
publicKey.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKeyBase64.Trim()), out _);
// 2. Fresh key pair
using var ephemeral = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256);
// 3. Derive shared AES Key
byte[] shared = ephemeral.DeriveRawSecretAgreement(publicKey.PublicKey);
byte[] aesKey = HKDF.DeriveKey(HashAlgorithmName.SHA256, shared, 32,
salt: Array.Empty<byte>(),
info: Encoding.UTF8.GetBytes("blinc-e2ee")
);
// 4. Encrypt with AES-256-GCM
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] ciphertext = new byte[plaintextBytes.Length];
byte[] tag = new byte[16];
using (var aes = new AesGcm(aesKey, tagSizeInBytes: 16))
{
aes.Encrypt(nonce, plaintextBytes, ciphertext, tag);
}
// 5. Extract Ephemeral public key: 0x04 || X || Y
ECParameters q = ephemeral.ExportParameters(false);
byte[] ephemeralPublic = new byte[65];
ephemeralPublic[0] = 0x04;
q.Q.X!.CopyTo(ephemeralPublic, 1);
q.Q.Y!.CopyTo(ephemeralPublic, 33);
CryptographicOperations.ZeroMemory(shared);
CryptographicOperations.ZeroMemory(aesKey);
// 6. Return envelope as Base64String
byte[] envelope = [.. ephemeralPublic, .. nonce, .. ciphertext, .. tag];
return Convert.ToBase64String(envelope);
}const { webcrypto } = require("crypto");
async function envelope(plaintext, publicKeyBase64) {
// 1. Import public key
const publicKey = await webcrypto.subtle.importKey(
"spki",
Buffer.from(publicKeyBase64.trim(), "base64"),
{ name: "ECDH", namedCurve: "P-256" },
false,
[]
);
// 2. Fresh key pair
const ephemeral = await webcrypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveBits"]
);
// 3. Derive shared AES key
const shared = await webcrypto.subtle.deriveBits(
{ name: "ECDH", public: publicKey },
ephemeral.privateKey,
256
);
const hkdfKey = await webcrypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
const aesKey = await webcrypto.subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(),
info: new TextEncoder().encode("blinc-e2ee"),
},
hkdfKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
);
// 4. Encrypt with AES-256-GCM
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
const plaintextBytes = new TextEncoder().encode(plaintext);
const encrypted = new Uint8Array(await webcrypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
aesKey,
plaintextBytes
));
const ciphertext = encrypted.slice(0, -16);
const tag = encrypted.slice(-16);
// 5. Extract ephemeral public key: 0x04 || X || Y
const ephemeralPublic = new Uint8Array(await webcrypto.subtle.exportKey("raw", ephemeral.publicKey));
// 6. Return envelope as Base64 string
return Buffer.concat([
Buffer.from(ephemeralPublic),
Buffer.from(nonce),
Buffer.from(ciphertext),
Buffer.from(tag),
]).toString("base64");
}import base64
import os
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
def envelope(plaintext: str, public_key_base64: str) -> str:
# 1. Import public key
public_key = serialization.load_der_public_key(
base64.b64decode(public_key_base64.strip())
)
# 2. Fresh key pair
ephemeral = ec.generate_private_key(ec.SECP256R1())
# 3. Derive shared AES key
shared = ephemeral.exchange(ec.ECDH(), public_key)
aes_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=b"",
info=b"blinc-e2ee",
).derive(shared)
# 4. Encrypt with AES-256-GCM
nonce = os.urandom(12)
plaintext_bytes = plaintext.encode("utf-8")
encrypted = AESGCM(aes_key).encrypt(nonce, plaintext_bytes, None)
ciphertext = encrypted[:-16]
tag = encrypted[-16:]
# 5. Extract ephemeral public key: 0x04 || X || Y
ephemeral_public = ephemeral.public_key().public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint,
)
# 6. Return envelope as Base64 string
return base64.b64encode(ephemeral_public + nonce + ciphertext + tag).decode("ascii")import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class BlincE2EE {
public static String envelope(String plaintext, String publicKeyBase64) throws Exception {
// 1. Import public key
byte[] publicKeyDer = Base64.getDecoder().decode(publicKeyBase64.trim());
ECPublicKey publicKey = (ECPublicKey) KeyFactory.getInstance("EC")
.generatePublic(new X509EncodedKeySpec(publicKeyDer));
// 2. Fresh key pair
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
generator.initialize(new ECGenParameterSpec("secp256r1"));
KeyPair ephemeral = generator.generateKeyPair();
// 3. Derive shared AES key
KeyAgreement agreement = KeyAgreement.getInstance("ECDH");
agreement.init(ephemeral.getPrivate());
agreement.doPhase(publicKey, true);
byte[] shared = agreement.generateSecret();
byte[] aesKey = hkdfSha256(shared, "blinc-e2ee".getBytes(StandardCharsets.UTF_8));
// 4. Encrypt with AES-256-GCM
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(128, nonce));
byte[] ciphertextAndTag = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
byte[] ciphertext = Arrays.copyOfRange(ciphertextAndTag, 0, ciphertextAndTag.length - 16);
byte[] tag = Arrays.copyOfRange(ciphertextAndTag, ciphertextAndTag.length - 16, ciphertextAndTag.length);
// 5. Extract ephemeral public key: 0x04 || X || Y
ECPublicKey ephemeralPublicKey = (ECPublicKey) ephemeral.getPublic();
byte[] ephemeralPublic = concat(new byte[] { 0x04 },
toFixed32(ephemeralPublicKey.getW().getAffineX()),
toFixed32(ephemeralPublicKey.getW().getAffineY()));
// 6. Return envelope as Base64 string
return Base64.getEncoder().encodeToString(concat(ephemeralPublic, nonce, ciphertext, tag));
}
private static byte[] hkdfSha256(byte[] secret, byte[] info) throws Exception {
byte[] prk = hmacSha256(new byte[32], secret);
return Arrays.copyOf(hmacSha256(prk, concat(info, new byte[] { 0x01 })), 32);
}
private static byte[] hmacSha256(byte[] key, byte[] data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
}
private static byte[] toFixed32(BigInteger value) {
byte[] bytes = value.toByteArray();
if (bytes.length == 32) return bytes;
if (bytes.length > 32) return Arrays.copyOfRange(bytes, bytes.length - 32, bytes.length);
byte[] padded = new byte[32];
System.arraycopy(bytes, 0, padded, 32 - bytes.length, bytes.length);
return padded;
}
private static byte[] concat(byte[]... parts) {
int length = 0;
for (byte[] part : parts) length += part.length;
byte[] output = new byte[length];
int offset = 0;
for (byte[] part : parts) {
System.arraycopy(part, 0, output, offset, part.length);
offset += part.length;
}
return output;
}
}package blince2ee
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"io"
)
func Envelope(plaintext string, publicKeyBase64 string) (string, error) {
// 1. Import public key
der, err := base64.StdEncoding.DecodeString(publicKeyBase64)
if err != nil {
return "", err
}
parsed, err := x509.ParsePKIXPublicKey(der)
if err != nil {
return "", err
}
ecdsaPublicKey := parsed.(*ecdsa.PublicKey)
publicKey, err := ecdsaPublicKey.ECDH()
if err != nil {
return "", err
}
// 2. Fresh key pair
ephemeral, err := ecdh.P256().GenerateKey(rand.Reader)
if err != nil {
return "", err
}
// 3. Derive shared AES key
shared, err := ephemeral.ECDH(publicKey)
if err != nil {
return "", err
}
aesKey := make([]byte, 32)
if _, err := io.ReadFull(hkdf.New(sha256.New, shared, []byte{}, []byte("blinc-e2ee")), aesKey); err != nil {
return "", err
}
// 4. Encrypt with AES-256-GCM
nonce := make([]byte, 12)
if _, err := rand.Read(nonce); err != nil {
return "", err
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
encrypted := gcm.Seal(nil, nonce, []byte(plaintext), nil)
ciphertext := encrypted[:len(encrypted)-16]
tag := encrypted[len(encrypted)-16:]
// 5. Extract ephemeral public key: 0x04 || X || Y
ephemeralPublic := ephemeral.PublicKey().Bytes()
// 6. Return envelope as Base64 string
envelope := append(append(append(ephemeralPublic, nonce...), ciphertext...), tag...)
return base64.StdEncoding.EncodeToString(envelope), nil
}Decrypt a request from Blinc
Use this flow when Blinc sends you an encrypted request. Decrypt the request body with the private key that matches the public key you shared with Blinc.
- Base64-decode the encrypted envelope.
- Read the first 65 bytes as the sender's single-use public key.
- Read the next 12 bytes as the nonce.
- Read the last 16 bytes as the GCM tag.
- Treat the bytes between the nonce and tag as the ciphertext.
- Use ECDH and HKDF-SHA256 with info
blinc-e2eeto recreate the AES key, then decrypt with AES-256-GCM.
Decrypt code examples
using System;
using System.Security.Cryptography;
using System.Text;
public static string DecryptEnvelope(string encryptedDataBase64, string privateKeyBase64)
{
// 1. Decode the Base64 envelope
byte[] envelope = Convert.FromBase64String(encryptedDataBase64);
// 2. Extract ephemeral public key
byte[] ephemeralPublic = envelope[..65];
// 3. Extract nonce
byte[] nonce = envelope[65..77];
// 4. Extract ciphertext
byte[] ciphertext = envelope[77..^16];
// 5. Extract authentication tag
byte[] tag = envelope[^16..];
// 6. Derive the AES key and decrypt
using var privateKey = ECDiffieHellman.Create();
privateKey.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKeyBase64.Trim()), out _);
ECParameters point = new()
{
Curve = ECCurve.NamedCurves.nistP256,
Q = new ECPoint
{
X = ephemeralPublic[1..33],
Y = ephemeralPublic[33..65]
}
};
using var ephemeral = ECDiffieHellman.Create(point);
byte[] shared = privateKey.DeriveRawSecretAgreement(ephemeral.PublicKey);
byte[] aesKey = HKDF.DeriveKey(HashAlgorithmName.SHA256, shared, 32,
salt: Array.Empty<byte>(),
info: Encoding.UTF8.GetBytes("blinc-e2ee")
);
byte[] plaintext = new byte[ciphertext.Length];
using (var aes = new AesGcm(aesKey, tagSizeInBytes: 16))
{
aes.Decrypt(nonce, ciphertext, tag, plaintext);
}
CryptographicOperations.ZeroMemory(shared);
CryptographicOperations.ZeroMemory(aesKey);
return Encoding.UTF8.GetString(plaintext);
}const { webcrypto } = require("crypto");
async function decryptEnvelope(encryptedDataBase64, privateKeyBase64) {
// 1. Decode the Base64 envelope
const envelope = Buffer.from(encryptedDataBase64, "base64");
// 2. Extract ephemeral public key
const ephemeralPublicBytes = envelope.subarray(0, 65);
// 3. Extract nonce
const nonce = envelope.subarray(65, 77);
// 4. Extract ciphertext
const ciphertext = envelope.subarray(77, envelope.length - 16);
// 5. Extract authentication tag
const tag = envelope.subarray(envelope.length - 16);
// 6. Derive the AES key and decrypt
const privateKey = await webcrypto.subtle.importKey(
"pkcs8",
Buffer.from(privateKeyBase64.trim(), "base64"),
{ name: "ECDH", namedCurve: "P-256" },
false,
["deriveBits"]
);
const ephemeralPublic = await webcrypto.subtle.importKey(
"raw",
ephemeralPublicBytes,
{ name: "ECDH", namedCurve: "P-256" },
false,
[]
);
const shared = await webcrypto.subtle.deriveBits(
{ name: "ECDH", public: ephemeralPublic },
privateKey,
256
);
const hkdfKey = await webcrypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
const aesKey = await webcrypto.subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(),
info: new TextEncoder().encode("blinc-e2ee"),
},
hkdfKey,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
const encrypted = Buffer.concat([ciphertext, tag]);
const plaintext = await webcrypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
aesKey,
encrypted
);
return new TextDecoder().decode(plaintext);
}import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
def decrypt_envelope(encrypted_data_base64: str, private_key_base64: str) -> str:
# 1. Decode the Base64 envelope
envelope = base64.b64decode(encrypted_data_base64)
# 2. Extract ephemeral public key
ephemeral_public_bytes = envelope[:65]
# 3. Extract nonce
nonce = envelope[65:77]
# 4. Extract ciphertext
ciphertext = envelope[77:-16]
# 5. Extract authentication tag
tag = envelope[-16:]
# 6. Derive the AES key and decrypt
private_key = serialization.load_der_private_key(
base64.b64decode(private_key_base64.strip()),
password=None,
)
ephemeral_public = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(),
ephemeral_public_bytes,
)
shared = private_key.exchange(ec.ECDH(), ephemeral_public)
aes_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=b"",
info=b"blinc-e2ee",
).derive(shared)
plaintext = AESGCM(aes_key).decrypt(nonce, ciphertext + tag, None)
return plaintext.decode("utf-8")import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class BlincE2EEDecrypt {
public static String decryptEnvelope(String encryptedDataBase64, String privateKeyBase64) throws Exception {
// 1. Decode the Base64 envelope
byte[] envelope = Base64.getDecoder().decode(encryptedDataBase64);
// 2. Extract ephemeral public key
byte[] ephemeralPublic = Arrays.copyOfRange(envelope, 0, 65);
// 3. Extract nonce
byte[] nonce = Arrays.copyOfRange(envelope, 65, 77);
// 4. Extract ciphertext
byte[] ciphertext = Arrays.copyOfRange(envelope, 77, envelope.length - 16);
// 5. Extract authentication tag
byte[] tag = Arrays.copyOfRange(envelope, envelope.length - 16, envelope.length);
// 6. Derive the AES key and decrypt
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey privateKey = keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyBase64.trim()))
);
AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
parameters.init(new ECGenParameterSpec("secp256r1"));
ECParameterSpec ecSpec = parameters.getParameterSpec(ECParameterSpec.class);
ECPoint point = new ECPoint(
new BigInteger(1, Arrays.copyOfRange(ephemeralPublic, 1, 33)),
new BigInteger(1, Arrays.copyOfRange(ephemeralPublic, 33, 65))
);
KeyAgreement agreement = KeyAgreement.getInstance("ECDH");
agreement.init(privateKey);
agreement.doPhase(keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec)), true);
byte[] shared = agreement.generateSecret();
byte[] aesKey = hkdfSha256(shared, "blinc-e2ee".getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(128, nonce));
byte[] plaintext = cipher.doFinal(concat(ciphertext, tag));
return new String(plaintext, StandardCharsets.UTF_8);
}
private static byte[] hkdfSha256(byte[] secret, byte[] info) throws Exception {
byte[] prk = hmacSha256(new byte[32], secret);
return Arrays.copyOf(hmacSha256(prk, concat(info, new byte[] { 0x01 })), 32);
}
private static byte[] hmacSha256(byte[] key, byte[] data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
}
private static byte[] concat(byte[]... parts) {
int length = 0;
for (byte[] part : parts) length += part.length;
byte[] output = new byte[length];
int offset = 0;
for (byte[] part : parts) {
System.arraycopy(part, 0, output, offset, part.length);
offset += part.length;
}
return output;
}
}package blince2ee
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/hkdf"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"io"
)
func DecryptEnvelope(encryptedDataBase64 string, privateKeyBase64 string) (string, error) {
// 1. Decode the Base64 envelope
envelope, err := base64.StdEncoding.DecodeString(encryptedDataBase64)
if err != nil {
return "", err
}
// 2. Extract ephemeral public key
ephemeralPublicBytes := envelope[:65]
// 3. Extract nonce
nonce := envelope[65:77]
// 4. Extract ciphertext
ciphertext := envelope[77 : len(envelope)-16]
// 5. Extract authentication tag
tag := envelope[len(envelope)-16:]
// 6. Derive the AES key and decrypt
privateKeyDer, err := base64.StdEncoding.DecodeString(privateKeyBase64)
if err != nil {
return "", err
}
parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(privateKeyDer)
if err != nil {
return "", err
}
ecdsaPrivateKey := parsedPrivateKey.(*ecdsa.PrivateKey)
privateKey, err := ecdsaPrivateKey.ECDH()
if err != nil {
return "", err
}
ephemeralPublic, err := ecdh.P256().NewPublicKey(ephemeralPublicBytes)
if err != nil {
return "", err
}
shared, err := privateKey.ECDH(ephemeralPublic)
if err != nil {
return "", err
}
aesKey := make([]byte, 32)
if _, err := io.ReadFull(hkdf.New(sha256.New, shared, []byte{}, []byte("blinc-e2ee")), aesKey); err != nil {
return "", err
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
encrypted := append(ciphertext, tag...)
plaintext, err := gcm.Open(nil, nonce, encrypted, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}Updated 4 days ago