A way to programmatically update your vault's contents — for technical people who want to automatically store the data they need kept safe, just in case.
IfGone provides a REST API so you can automate vault updates from scripts, servers, and scheduled tasks. Whether it's backing up a manuscript you're working on, saving the latest draft of important instructions, or keeping your emergency contacts up to date — your systems can update vault slots without opening a browser.
Your vault has 10 encrypted slots (0–9), each holding up to 10 MB. Each slot can be updated once per month. The server never sees your plaintext: you encrypt data locally using your account's public key, then upload the ciphertext. Every request is signed with your private key so the server knows it came from you.
Every write request must be signed with your ECDSA-SHA256 private key (P-256 curve). The signature proves the request came from you without transmitting the private key.
{timestamp}:{bodyHash} — where timestamp is a Unix timestamp in seconds (UTC). The timestamp serves as a nonce to prevent replay attacks and must be UTC-based to prevent timezone manipulation.| Header | Description |
|---|---|
X-Account-Id | Your account ID (e.g. acc_xxxxxxxxxxxx). Required on all authenticated endpoints. Sent as a header, not in the request body. |
X-Signature | Base64-encoded DER ECDSA-SHA256 signature |
X-Timestamp | Unix timestamp in seconds, UTC (must be within 5 minutes of server time) |
The private key is the same one generated during vault setup and stored on your physical token.
Slot data must be encrypted before uploading. IfGone never receives plaintext. The encryption scheme is ECIES (Elliptic Curve Integrated Encryption Scheme) using the P-256 curve:
"ifgone-slot-encryption".| Field | Description |
|---|---|
encryptedData | Base64-encoded AES-256-GCM ciphertext + authentication tag |
encryptedDek | Base64-encoded ephemeral public key (raw uncompressed P-256 point, 65 bytes starting with 0x04) |
iv | Base64-encoded 12-byte AES-GCM initialization vector |
salt | Base64-encoded 16-byte HKDF salt |
The account's public key is available from the public account endpoint — no authentication required to fetch it.
/api/v1/slot/:slotId/update
Overwrites a slot's encrypted contents. slotId must be 0–9. Requires signature. Rate limited to 1 update per slot per 30 days. Account ID is provided via the X-Account-Id header.
{
"encryptedData": "base64...",
"encryptedDek": "base64...",
"iv": "base64...",
"salt": "base64..."
}
{
"slotId": 0,
"sizeBytes": 2048,
"updatedAt": "2025-06-01T12:00:00.000Z",
"nextUpdateAvailable": "2025-07-01T12:00:00.000Z"
}
| Code | Meaning | When |
|---|---|---|
202 | Accepted | Slot updated successfully |
400 | Bad Request | Invalid slotId, missing fields, exceeds 10 MB |
401 | Unauthorized | Missing/invalid signature or timestamp |
402 | Payment Required | Account not funded |
429 | Too Many Requests | Rate limit (1 update per slot per 30 days) |
500 | Server Error | Internal error |
/api/v1/slots
Lists all slots with metadata (size, last updated, next update available). Requires X-Account-Id, X-Signature, and X-Timestamp headers.
| Header | Description |
|---|---|
X-Account-Id | Your account ID |
X-Signature | Base64-encoded DER ECDSA-SHA256 signature |
X-Timestamp | Unix timestamp in seconds, UTC (within 5 minutes) |
{
"accountId": "acc_xxxxxxxxxxxx",
"ownerEmail": "user@example.com",
"vetoWindowHours": 72,
"slots": [
{
"slotId": 0,
"sizeBytes": 2048,
"lastUpdated": "2025-06-01T12:00:00.000Z",
"nextUpdateAvailable": "2025-07-01T12:00:00.000Z",
"label": "Server Credentials"
}
]
}
| Code | Meaning | When |
|---|---|---|
200 | OK | Slots listed successfully |
401 | Unauthorized | Missing/invalid signature or timestamp |
402 | Payment Required | Account not funded |
500 | Server Error | Internal error |
/api/v1/slot/:slotId/label
Updates a slot's display label (max 20 characters). Requires signature. Rate limited to 1 change per slot per 30 days. Account ID is provided via the X-Account-Id header.
{
"label": "Server Credentials"
}
| Code | Meaning | When |
|---|---|---|
200 | OK | Label updated successfully |
400 | Bad Request | Invalid slotId, label missing or exceeds 20 characters |
401 | Unauthorized | Missing/invalid signature or timestamp |
402 | Payment Required | Account not funded |
429 | Too Many Requests | Rate limit (1 label change per slot per 30 days) |
500 | Server Error | Internal error |
/api/v1/slot/:slotId/retrieve
Initiates retrieval for a single slot. slotId must be 0–9. Requires X-Account-Id, X-Signature, and X-Timestamp headers.
| Header | Description |
|---|---|
X-Account-Id | Your account ID |
X-Signature | Base64-encoded DER ECDSA-SHA256 signature |
X-Timestamp | Unix timestamp in seconds, UTC (within 5 minutes) |
{
"executorEmail": "executor@example.com"
}
{
"retrievalId": "ret_xxxxxxxxxxxx",
"slotId": 0,
"requestedAt": "2025-06-01T12:00:00.000Z",
"vetoDeadline": "2025-06-04T12:00:00.000Z",
"executorEmail": "executor@example.com"
}
| Code | Meaning | When |
|---|---|---|
202 | Accepted | Slot retrieval initiated |
400 | Bad Request | Invalid slotId or missing executor email |
401 | Unauthorized | Missing/invalid signature or timestamp |
402 | Payment Required | Account not funded |
500 | Server Error | Internal error |
/api/v1/retrieve
Initiates whole-vault retrieval (all slots). Requires X-Account-Id, X-Signature, and X-Timestamp headers.
| Header | Description |
|---|---|
X-Account-Id | Your account ID |
X-Signature | Base64-encoded DER ECDSA-SHA256 signature |
X-Timestamp | Unix timestamp in seconds, UTC (within 5 minutes) |
{
"executorEmail": "executor@example.com"
}
{
"retrievalId": "ret_xxxxxxxxxxxx",
"requestedAt": "2025-06-01T12:00:00.000Z",
"vetoDeadline": "2025-06-04T12:00:00.000Z",
"executorEmail": "executor@example.com"
}
| Code | Meaning | When |
|---|---|---|
202 | Accepted | Vault retrieval initiated |
400 | Bad Request | Missing executor email |
401 | Unauthorized | Missing/invalid signature or timestamp |
402 | Payment Required | Account not funded |
500 | Server Error | Internal error |
These endpoints allow you to change your account's owner email address and veto window. Changes require a confirmation flow: after requesting a change, a confirmation email is sent to your current address. You must approve the change within the timeout period for it to take effect.
/api/v1/account/change-request
Initiates a change to the account's owner email and/or veto window. Requires a signed request. A confirmation email is sent to the current owner email address — the change must be approved within the timeout period. At least one of ownerEmail or vetoWindowHours must be provided.
| Header | Description |
|---|---|
X-Account-Id | Your account ID |
X-Signature | Base64-encoded DER ECDSA-SHA256 signature |
X-Timestamp | Unix timestamp in seconds, UTC (within 5 minutes) |
| Field | Type | Required | Description |
|---|---|---|---|
ownerEmail | string | No* | New owner email address. Must be a valid email format. |
vetoWindowHours | number | No* | New veto window in hours. Must be between 48 and 2160. |
* At least one of ownerEmail or vetoWindowHours must be provided.
{
"status": "pending",
"expiresAt": "2025-06-02T12:00:00.000Z"
}
| Code | Meaning | When |
|---|---|---|
202 | Accepted | Change request created, confirmation email sent |
400 | Bad Request | Invalid email format, vetoWindowHours out of range, or no fields provided |
401 | Unauthorized | Missing/invalid signature or timestamp |
409 | Conflict | A change request is already pending |
500 | Server Error | Internal error |
/api/v1/account/pending-change
Checks whether a settings change is currently pending for the account. Requires a signed request.
| Header | Description |
|---|---|
X-Account-Id | Your account ID |
X-Signature | Base64-encoded DER ECDSA-SHA256 signature |
X-Timestamp | Unix timestamp in seconds, UTC (within 5 minutes) |
{
"pending": true,
"ownerEmail": "new@email.com",
"vetoWindowHours": 168,
"requestedAt": "2025-06-01T12:00:00.000Z",
"expiresAt": "2025-06-02T12:00:00.000Z"
}
{
"pending": false
}
| Code | Meaning | When |
|---|---|---|
200 | OK | Pending change status returned |
401 | Unauthorized | Missing/invalid signature or timestamp |
500 | Server Error | Internal error |
/api/v1/account/pending-change
Cancels a pending settings change. Requires a signed request.
| Header | Description |
|---|---|
X-Account-Id | Your account ID |
X-Signature | Base64-encoded DER ECDSA-SHA256 signature |
X-Timestamp | Unix timestamp in seconds, UTC (within 5 minutes) |
{
"cancelled": true
}
| Code | Meaning | When |
|---|---|---|
200 | OK | Pending change cancelled successfully |
401 | Unauthorized | Missing/invalid signature or timestamp |
500 | Server Error | Internal error |
/api/v1/account/approve-change
Approves a pending settings change using the token from the confirmation email. This endpoint uses token-based authentication — no signature is required.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Approval token from the confirmation email |
{
"accountId": "acc_xxxxxxxxxxxx",
"ownerEmail": "new@email.com",
"vetoWindowHours": 168,
"updatedAt": "2025-06-01T13:00:00.000Z"
}
| Code | Meaning | When |
|---|---|---|
200 | OK | Change applied successfully |
400 | Bad Request | Invalid or expired token |
404 | Not Found | No pending change found for this token |
500 | Server Error | Internal error |
The following example shows the full workflow for changing an account's owner email address.
curl -X POST https://ifgone.io/api/v1/account/change-request \
-H "Content-Type: application/json" \
-H "X-Account-Id: acc_xxxxxxxxxxxx" \
-H "X-Signature: BASE64_DER_SIGNATURE" \
-H "X-Timestamp: 1748764800" \
-d '{"ownerEmail":"new@email.com"}'
curl https://ifgone.io/api/v1/account/pending-change \
-H "X-Account-Id: acc_xxxxxxxxxxxx" \
-H "X-Signature: BASE64_DER_SIGNATURE" \
-H "X-Timestamp: 1748764860"
curl -X POST https://ifgone.io/api/v1/account/approve-change \
-H "Content-Type: application/json" \
-d '{"token":"TOKEN_FROM_EMAIL"}'
curl -X DELETE https://ifgone.io/api/v1/account/pending-change \
-H "X-Account-Id: acc_xxxxxxxxxxxx" \
-H "X-Signature: BASE64_DER_SIGNATURE" \
-H "X-Timestamp: 1748764920"
Overwriting a slot replaces its encrypted contents entirely. Each update generates a new ephemeral ECDH key pair for that specific operation. The ephemeral key is used only for key agreement — it is not the private key, and it cannot be used to decrypt. The server stores only the encrypted payload; it never sees plaintext, never sees the private key, and cannot decrypt.
The private key is the only thing needed to decrypt (alongside the vault data). An executor with only the QR code (private key) can decrypt any slot — the ephemeral public key, salt, and IV are all stored in the vault.
POST /api/v1/slot/:slotId/updateComplete examples for updating a slot in four languages. Each example shows encryption, signing, and the HTTP request.
Uses Node.js built-in crypto module. No additional dependencies.
BASE_URL, ACCOUNT_ID, and PRIVATE_KEY_PEM for simplicity. Load these from environment variables or a secrets manager instead. Error handling is also omitted for brevity.
import crypto from 'crypto';
const BASE_URL = 'https://ifgone.io';
const ACCOUNT_ID = 'acc_xxxxxxxxxxxx';
const PRIVATE_KEY_PEM = `[REDACTED PRIVATE KEY]`;
// --- 1. Sign a request (ECDSA-SHA256, P-256, DER-encoded) ---
function signRequest(body: string): { timestamp: string; signature: string } {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const message = `${timestamp}:${bodyHash}`;
const signKey = crypto.createPrivateKey(PRIVATE_KEY_PEM);
const sig = crypto.sign('sha256', Buffer.from(message), signKey);
// Node.js crypto.sign() produces DER-encoded signatures natively
return { timestamp, signature: sig.toString('base64') };
}
// --- 2. Encrypt slot data (ECIES: ECDH + HKDF-SHA256 + AES-256-GCM) ---
function encryptSlot(plaintext: string, publicKeyPem: string) {
const recipientPub = crypto.createPublicKey(publicKeyPem);
// Generate ephemeral ECDH key pair
const { publicKey: ephPub, privateKey: ephPriv } =
crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
// Derive shared secret via ECDH
const sharedSecret = crypto.diffieHellman({
publicKey: recipientPub,
privateKey: ephPriv,
});
// Derive AES-256 key via HKDF-SHA256
const salt = crypto.randomBytes(16);
const aesKey = crypto.hkdfSync(
'sha256', sharedSecret, salt,
Buffer.from('ifgone-slot-encryption'), 32,
);
// Encrypt with AES-256-GCM
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
const ciphertext = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
// Export ephemeral public key as raw uncompressed point (65 bytes)
const ephPubDer = ephPub.export({ type: 'spki', format: 'der' });
const ephPubRaw = ephPubDer.slice(-65);
return {
encryptedData: Buffer.concat([ciphertext, authTag]).toString('base64'),
encryptedDek: ephPubRaw.toString('base64'),
iv: iv.toString('base64'),
salt: salt.toString('base64'),
};
}
// --- 3. Update a slot ---
async function updateSlot(slotId: number, content: string) {
// Use the public key stored locally from account creation
// (The public account lookup endpoint has been removed for security)
const publicKey = LOCAL_PUBLIC_KEY;
// Encrypt the content
const encrypted = encryptSlot(content, publicKey);
// Build and sign the request (accountId goes in header, not body)
const body = JSON.stringify(encrypted);
const { timestamp, signature } = signRequest(body);
// Send the update
const res = await fetch(`${BASE_URL}/api/v1/slot/${slotId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Account-Id': ACCOUNT_ID,
'X-Signature': signature,
'X-Timestamp': timestamp,
},
body,
});
return res.json();
}
// Overwrite slot 0 with new content
updateSlot(0, 'My updated secret data').then(console.log);
Uses .NET 8+ built-in crypto APIs. No external dependencies.
accountId and baseUrl for simplicity. Load these from environment variables or a configuration file. Error handling is omitted for brevity.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var baseUrl = "https://ifgone.io";
var accountId = "acc_xxxxxxxxxxxx";
var privateKeyPem = await File.ReadAllTextAsync("private-key.pem");
// --- 1. Sign a request (ECDSA-SHA256, P-256, DER-encoded) ---
(byte[] sig, string ts) SignRequest(string body)
{
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var bodyHash = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(body)))
.ToLowerInvariant();
var message = $"{ts}:{bodyHash}";
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
ecdsa.ImportFromPem(privateKeyPem);
return (ecdsa.SignData(
Encoding.UTF8.GetBytes(message),
HashAlgorithmName.SHA256), ts);
}
// --- 2. Encrypt slot data (ECIES: ECDH + HKDF-SHA256 + AES-256-GCM) ---
EncryptedSlot EncryptSlot(string plaintext, string publicKeyPem)
{
// Import recipient public key into ECDH
using var ecdsaPub = ECDsa.Create();
ecdsaPub.ImportFromPem(publicKeyPem);
var pubParams = ecdsaPub.ExportParameters(false);
using var recipientEcdh =
ECDiffieHellman.Create(pubParams.Curve);
recipientEcdh.ImportParameters(new ECParameters {
Q = pubParams.Q, Curve = pubParams.Curve });
// Generate ephemeral key pair
using var eph = ECDiffieHellman.Create(
ECCurve.NamedCurves.nistP256);
// Derive shared secret
var sharedSecret = eph.DeriveRawSecret(recipientEcdh.PublicKey);
// HKDF-SHA256
var salt = RandomNumberGenerator.GetBytes(16);
var aesKey = HKDF.DeriveKey(HashAlgorithmName.SHA256,
sharedSecret, 32, salt,
Encoding.UTF8.GetBytes("ifgone-slot-encryption"));
// AES-256-GCM
var iv = RandomNumberGenerator.GetBytes(12);
var ptBytes = Encoding.UTF8.GetBytes(plaintext);
var ciphertext = new byte[ptBytes.Length];
var tag = new byte[16];
using var aes = new AesGcm(aesKey, 16);
aes.Encrypt(iv, ptBytes, ciphertext, tag);
// Export ephemeral public key (raw uncompressed point, 65 bytes)
var ephP = eph.ExportParameters(false);
var rawPoint = new byte[1 + ephP.Q.X!.Length + ephP.Q.Y!.Length];
rawPoint[0] = 0x04;
ephP.Q.X.CopyTo(rawPoint, 1);
ephP.Q.Y.CopyTo(rawPoint, 1 + ephP.Q.X.Length);
return new EncryptedSlot(
Convert.ToBase64String(ciphertext.Concat(tag).ToArray()),
Convert.ToBase64String(rawPoint),
Convert.ToBase64String(iv),
Convert.ToBase64String(salt));
}
record EncryptedSlot(
string EncryptedData, string EncryptedDek,
string Iv, string Salt);
// --- 3. Update a slot ---
// Use the public key stored locally from account creation
// (The public account lookup endpoint has been removed for security)
var publicKey = LOCAL_PUBLIC_KEY;
var encrypted = EncryptSlot("My encrypted content", publicKey);
// accountId goes in X-Account-Id header, not the body
var body = JsonSerializer.Serialize(new Dictionary<string, object> {
["encryptedData"] = encrypted.EncryptedData,
["encryptedDek"] = encrypted.EncryptedDek,
["iv"] = encrypted.Iv,
["salt"] = encrypted.Salt,
});
var (sig, ts) = SignRequest(body);
var request = new HttpRequestMessage(HttpMethod.Post,
$"{baseUrl}/api/v1/slot/0/update");
request.Headers.Add("X-Account-Id", accountId);
request.Headers.Add("X-Signature", Convert.ToBase64String(sig));
request.Headers.Add("X-Timestamp", ts);
request.Content = new StringContent(body, Encoding.UTF8,
"application/json");
var response = await http.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Requires p256, aes-gcm, hkdf, sha2, reqwest, and base64 crates.
BASE_URL and ACCOUNT_ID constants for simplicity. Load these from environment variables or a config file. The example also uses .unwrap() everywhere instead of proper error handling, and does not validate inputs.
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use hkdf::Hkdf;
use p256::{
ecdsa::{signature::Signer, DerSignature, SigningKey},
ecdh::EphemeralSecret,
EncodedPoint, PublicKey,
};
use rand::rngs::OsRng;
use sha2::{Digest, Sha256};
const BASE_URL: &str = "https://ifgone.io";
const ACCOUNT_ID: &str = "acc_xxxxxxxxxxxx";
// --- 1. Sign a request (ECDSA-SHA256, P-256, DER-encoded) ---
fn sign_request(
body: &str,
signing_key: &SigningKey,
) -> (String, String) {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string();
let body_hash = hex::encode(Sha256::digest(body.as_bytes()));
let message = format!("{ts}:{body_hash}");
let sig: DerSignature = signing_key.sign(message.as_bytes());
(B64.encode(sig.to_bytes()), ts)
}
// --- 2. Encrypt slot data (ECIES: ECDH + HKDF-SHA256 + AES-256-GCM) ---
struct EncryptedSlot {
encrypted_data: String,
encrypted_dek: String,
iv: String,
salt: String,
}
fn encrypt_slot(plaintext: &str, recipient_pub: &PublicKey) -> EncryptedSlot {
// Generate ephemeral ECDH key pair
let eph_secret = EphemeralSecret::random(&mut OsRng);
let eph_public = eph_secret.public_key();
// Derive shared secret via ECDH
let shared = eph_secret.diffie_hellman(recipient_pub);
// HKDF-SHA256 to derive AES-256 key
let salt: [u8; 16] = rand::random();
let hkdf = Hkdf::<Sha256>::new(
Some(&salt), shared.raw_secret_bytes());
let mut aes_key = [0u8; 32];
hkdf.expand(b"ifgone-slot-encryption", &mut aes_key).unwrap();
// AES-256-GCM
let iv: [u8; 12] = rand::random();
let cipher = Aes256Gcm::new_from_slice(&aes_key).unwrap();
let ciphertext = cipher
.encrypt(Nonce::from_slice(&iv), plaintext.as_bytes())
.unwrap();
// Export ephemeral public key (raw uncompressed, 65 bytes)
let eph_bytes = eph_public.to_encoded_point(false);
EncryptedSlot {
encrypted_data: B64.encode(&ciphertext),
encrypted_dek: B64.encode(eph_bytes.as_bytes()),
iv: B64.encode(iv),
salt: B64.encode(salt),
}
}
// --- 3. Update a slot (using reqwest) ---
async fn update_slot(
slot_id: u8,
content: &str,
signing_key: &SigningKey,
) -> serde_json::Value {
let client = reqwest::Client::new();
// Use the public key stored locally from account creation
// (The public account lookup endpoint has been removed for security)
let pub_key: PublicKey = LOCAL_PUBLIC_KEY.parse().unwrap();
// Encrypt and sign (accountId goes in header, not body)
let enc = encrypt_slot(content, &pub_key);
let body = serde_json::json!({
"encryptedData": enc.encrypted_data,
"encryptedDek": enc.encrypted_dek,
"iv": enc.iv,
"salt": enc.salt,
});
let body_str = serde_json::to_string(&body).unwrap();
let (sig, ts) = sign_request(&body_str, signing_key);
client.post(format!("{BASE_URL}/api/v1/slot/{slot_id}/update"))
.header("X-Account-Id", ACCOUNT_ID)
.header("X-Signature", sig)
.header("X-Timestamp", ts)
.header("Content-Type", "application/json")
.body(body_str)
.send().await.unwrap()
.json().await.unwrap()
}
Uses JDK 17+ built-in crypto. Includes a self-contained HKDF implementation (no external dependencies required).
BASE_URL and ACCOUNT_ID constants for simplicity. Load these from environment variables or a secure configuration source. Error handling and input validation are omitted for brevity.
import java.math.BigInteger;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.*;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class IfGoneApi {
static final String BASE_URL = "https://ifgone.io";
static final String ACCOUNT_ID = "acc_xxxxxxxxxxxx";
// --- 1. Sign a request (ECDSA-SHA256, P-256, DER-encoded) ---
static String[] signRequest(String body, PrivateKey signKey)
throws Exception {
String ts = String.valueOf(
Instant.now().getEpochSecond());
var sha256 = MessageDigest.getInstance("SHA-256");
String bodyHash = hexEncode(
sha256.digest(body.getBytes(StandardCharsets.UTF_8)));
String message = ts + ":" + bodyHash;
var ecdsa = Signature.getInstance("SHA256withECDSA");
ecdsa.initSign(signKey);
ecdsa.update(message.getBytes(StandardCharsets.UTF_8));
String sig = Base64.getEncoder()
.encodeToString(ecdsa.sign()); // DER-encoded by default
return new String[] { sig, ts };
}
// --- 2. Encrypt slot data (ECIES: ECDH + HKDF-SHA256 + AES-256-GCM) ---
static String[] encryptSlot(String plaintext, PublicKey recipientPub)
throws Exception {
// Generate ephemeral EC key pair
var kpg = KeyPairGenerator.getInstance("EC");
kpg.initialize(new ECGenParameterSpec("secp256r1"));
var eph = kpg.generateKeyPair();
// ECDH key agreement
var ka = KeyAgreement.getInstance("ECDH");
ka.init(eph.getPrivate());
ka.doPhase(recipientPub, true);
byte[] sharedSecret = ka.generateSecret();
// HKDF-SHA256 key derivation
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);
byte[] aesKey = hkdfSha256(sharedSecret, salt,
"ifgone-slot-encryption"
.getBytes(StandardCharsets.UTF_8), 32);
// AES-256-GCM
byte[] iv = new byte[12];
SecureRandom.getInstanceStrong().nextBytes(iv);
var cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(aesKey, "AES"),
new GCMParameterSpec(128, iv));
byte[] ctAndTag = cipher.doFinal(
plaintext.getBytes(StandardCharsets.UTF_8));
// Export ephemeral public key (raw uncompressed, 65 bytes)
byte[] ephPubDer = eph.getPublic().getEncoded();
byte[] ephPubRaw = Arrays.copyOfRange(
ephPubDer, ephPubDer.length - 65, ephPubDer.length);
var enc = Base64.getEncoder();
return new String[] {
enc.encodeToString(ctAndTag), // encryptedData
enc.encodeToString(ephPubRaw), // encryptedDek
enc.encodeToString(iv), // iv
enc.encodeToString(salt), // salt
};
}
// HKDF-SHA256 (extract-then-expand)
static byte[] hkdfSha256(byte[] ikm, byte[] salt,
byte[] info, int length) throws Exception {
// Extract: PRK = HMAC-SHA256(salt, IKM)
var hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(
salt.length == 0 ? new byte[32] : salt, "HmacSHA256"));
byte[] prk = hmac.doFinal(ikm);
// Expand: output blocks
hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(prk, "HmacSHA256"));
byte[] result = new byte[length];
byte[] t = new byte[0];
int off = 0;
for (int i = 1; off < length; i++) {
hmac.reset();
hmac.update(t);
hmac.update(info);
hmac.update((byte) i);
t = hmac.doFinal();
System.arraycopy(t, 0, result, off,
Math.min(t.length, length - off));
off += t.length;
}
return result;
}
static String hexEncode(byte[] bytes) {
var sb = new StringBuilder();
for (byte b : bytes) sb.append(String.format("%02x", b));
return sb.toString();
}
// --- 3. Update a slot ---
public static void main(String[] args) throws Exception {
var client = HttpClient.newHttpClient();
// Load private key
var keyPem = Files.readString(Path.of("private-key.pem"));
var b64 = keyPem.replace("[REDACTED PRIVATE KEY]", "")
.replaceAll("\\s", "");
var privKey = KeyFactory.getInstance("EC")
.generatePrivate(new PKCS8EncodedKeySpec(
Base64.getDecoder().decode(b64)));
// Use the public key stored locally from account creation
// (The public account lookup endpoint has been removed for security)
var pubB64 = LOCAL_PUBLIC_KEY_B64;
var pubKey = KeyFactory.getInstance("EC")
.generatePublic(new X509EncodedKeySpec(
Base64.getMimeDecoder().decode(pubB64)));
// Encrypt, sign, and send (accountId goes in header, not body)
var enc = encryptSlot("My encrypted content", pubKey);
var body = String.format(
"{\"encryptedData\":\"%s\","
+ "\"encryptedDek\":\"%s\",\"iv\":\"%s\",\"salt\":\"%s\"}",
enc[0], enc[1], enc[2], enc[3]);
var sigTs = signRequest(body, privKey);
var req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/v1/slot/0/update"))
.header("Content-Type", "application/json")
.header("X-Account-Id", ACCOUNT_ID)
.header("X-Signature", sigTs[0])
.header("X-Timestamp", sigTs[1])
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var res = client.send(req,
HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
Local development build — data stored on this machine