Skip to main content

Reencryption

At a high level, reencryption in SPF transforms a ciphertext decryptable with respect to the committee’s private key to a ciphertext decryptable by a particular user’s private key. What’s magical about reencryption is that we can accomplish this transformation without ever decrypting the ciphertext!

Reencryption is useful for users who worry about the possibility that a majority of the threshold committee may collude to decrypt network data that aren’t supposed to.

Today, our implementation relies on one-time pads. Currently, re-encryption is only available for web2 programs.

Ensure the client is initialized before using these functions.

How reencryption works

The standard decryption workflow requires a network round-trip to the SPF threshold committee:

  1. Request threshold decryption
  2. Wait for committee to decrypt
  3. Receive plaintext result

With reencryption, the final decryption happens locally:

  1. Generate one-time pad (OTP) keypair
  2. Request reencryption with public OTP
  3. Request threshold decryption of reencrypted ciphertext
  4. Locally decrypt with secret OTP (instant)

Generating OTP keypair

To generate a one-time pad keypair, use the generateOtp() function:

export async function generateOtpExample(): Promise<spf.OtpKeypair> {
// Generate OTP keypair
const { publicOtp, secretOtp } = await spf.generateOtp();

console.log("Public OTP:", publicOtp.length, "bytes");
console.log("Secret OTP:", secretOtp.length, "bytes");

// Keep secretOtp private! Only share publicOtp with SPF
return { publicOtp, secretOtp };
}

The function generates a keypair where the public OTP is FHE-encrypted and sent to SPF for reencryption, while the secret OTP is kept locally for final decryption.

Returns: Promise<OtpKeypair> - Object with publicOtp and secretOtp (both Uint8Array)

Security note: The secret OTP must remain private. Only share the public OTP with SPF.

Requesting reencryption

To request reencryption of a ciphertext using a one-time pad, use the requestReencryption() function:

export async function requestRecryptionExample(
signer: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
publicOtp: Uint8Array,
): Promise<spf.ReencryptHandle> {
// Request reencryption with the public OTP
const reencryptHandle = await spf.requestReencryption(
signer,
ciphertextId,
publicOtp,
);

console.log("Reencryption requested:", reencryptHandle);

return reencryptHandle;
}

This submits a reencryption request to SPF. The service reencrypts the ciphertext under the provided public OTP, enabling local decryption with the corresponding secret OTP. The signer must have decrypt access to the ciphertext.

Parameters:

  • signer: AnySigner - Signer for authentication (must have decrypt access)
  • ciphertextId: CiphertextId - The ciphertext identifier to reencrypt
  • publicOtp: Uint8Array - The public one-time pad bytes from generateOtp()

Returns: Promise<ReencryptHandle> - The reencryption handle (branded hex string with 0x prefix)

Checking reencryption status

To check the status of a reencryption request, use the checkReencryptionStatus() function:

const status = await spf.checkReencryptionStatus(reencryptHandle);

if (status.status === "success") {
console.log("Reencrypted ciphertext ID:", status.payload.id);
} else if (status.status === "failed") {
console.error("Reencryption failed:", status.payload?.message);
}

Parameters:

  • reencryptHandle: ReencryptHandle - The reencryption handle from requestReencryption()

Returns: Promise<ReencryptionStatus> - Discriminated union with status field

Waiting for reencryption completion

For convenience, use waitForReencryption() to automatically poll until reencryption completes:

export async function waitForRecryptionExample(
reencryptHandle: spf.ReencryptHandle,
): Promise<spf.CiphertextId> {
// Wait for reencryption to complete
const reencryptedCtId = await spf.waitForReencryption(reencryptHandle);

console.log("Reencrypted ciphertext ID:", reencryptedCtId);

return reencryptedCtId;
}

This function polls checkReencryptionStatus() until reencryption succeeds or fails, then returns the reencrypted ciphertext ID.

Parameters:

  • reencryptHandle: ReencryptHandle - The reencryption handle from requestReencryption()
  • signal: AbortSignal - Optional cancellation signal (use AbortSignal.timeout(ms) for timeout)

Returns: Promise<CiphertextId> - The reencrypted ciphertext identifier

Cancellation support

The waitForReencryption() function supports cancellation via AbortSignal:

// Wait with 30 second timeout
const reencryptedCtId = await spf.waitForReencryption(
reencryptHandle,
AbortSignal.timeout(30000),
);

Local OTP decryption

After reencryption completes, use the secret OTP to decrypt locally:

export async function otpDecryptExample(
signer: spf.PrivateKeySigner,
reencryptedCtId: spf.CiphertextId,
secretOtp: Uint8Array,
bitWidth: spf.BitWidth,
): Promise<bigint> {
// Request threshold decryption of the reencrypted ciphertext
const decryptHandle = await spf.requestDecryption(signer, reencryptedCtId);

// Get OTP-encrypted polynomial bytes (NOT parsed plaintext)
const otpEncryptedPoly = await spf.getPolynomialBytesForOtp(decryptHandle);

// Perform local OTP decryption with the secret OTP
const plaintext: bigint = await spf.otpDecrypt(
otpEncryptedPoly,
secretOtp,
bitWidth,
false,
);

console.log("Decrypted value:", plaintext);

return plaintext;
}

This workflow performs threshold decryption on the reencrypted ciphertext, retrieves the OTP-encrypted polynomial bytes, and locally decrypts using the secret OTP.

Key functions:

  • requestDecryption(): Request threshold decryption of the reencrypted ciphertext (returns DecryptHandle)
  • getPolynomialBytesForOtp(): Wait for threshold decryption and retrieve OTP-encrypted polynomial bytes (not parsed plaintext)
  • otpDecrypt(): Perform instant local decryption using the secret OTP

getPolynomialBytesForOtp() parameters:

  • decryptHandle: DecryptHandle - The decrypt handle from requestDecryption()
  • signal: AbortSignal - Optional cancellation signal (use AbortSignal.timeout(ms) for timeout)

otpDecrypt() parameters:

  • polyBytes: Uint8Array - The OTP-encrypted polynomial bytes from getPolynomialBytesForOtp()
  • secretOtp: Uint8Array - The secret one-time pad bytes from generateOtp()
  • bitWidth: 8 | 16 | 32 | 64 - The bit width of the encrypted value
  • signed: boolean - Whether the value is signed (default: false)

Returns: Promise<bigint> - The decrypted plaintext value

Complete reencryption workflow

Here is a complete example demonstrating the entire reencryption workflow:

export async function completeRecryptionWorkflow(
signer: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
bitWidth: spf.BitWidth = 16,
): Promise<bigint> {
// Step 1: Generate OTP keypair
const { publicOtp, secretOtp } = await spf.generateOtp();

// Step 2: Request reencryption
const reencryptHandle = await spf.requestReencryption(
signer,
ciphertextId,
publicOtp,
);

// Step 3: Wait for reencryption
const reencryptedCtId = await spf.waitForReencryption(reencryptHandle);

// Step 4: Request threshold decryption of the reencrypted ciphertext
const decryptHandle = await spf.requestDecryption(signer, reencryptedCtId);

// Step 5: Get OTP-encrypted polynomial bytes
const otpEncryptedPoly = await spf.getPolynomialBytesForOtp(decryptHandle);

// Step 6: Perform local OTP decryption
const plaintext = await spf.otpDecrypt(
otpEncryptedPoly,
secretOtp,
bitWidth,
false,
);

console.log("Final plaintext:", plaintext);

return plaintext;
}

This workflow:

  1. Generates an OTP keypair
  2. Requests reencryption with the public OTP
  3. Waits for reencryption to complete
  4. Requests threshold decryption of the reencrypted ciphertext
  5. Retrieves OTP-encrypted polynomial bytes
  6. Performs instant local decryption with the secret OTP