Skip to content

Checksums and integrity

Lockwell verifies object integrity end to end with five algorithms: CRC32, CRC32C, CRC64NVME, SHA1, and SHA256. The flow is the same for all of them:

  • The SDK computes the digest over the exact bytes you upload and sends it on the request.
  • The server recomputes it over the bytes it received and rejects a mismatch before anything is committed.
  • The verified digest is stored and echoed back, so later reads return it for a downstream consumer to re-check.

Pick an algorithm by need. CRC64NVME is the modern default: fast and covering the full object. Use SHA256 when you need a cryptographic digest, for an external attestation, a content-addressed lookup, or a regulatory requirement. The CRC variants are cheaper on the CPU and are the right pick for throughput.

Put an object with a verified checksum

The native client takes checksums as a map of algorithm name to a precomputed base64 digest. Compute the digest yourself (or with the helpers below) and pass it. The server verifies it before any bytes are committed, then echoes it back on the result.

ts
import { NativeClient, computeChecksumBase64 } from "@kelphect/sdk";

const native = new NativeClient({
  endpoint: "https://objects.example.com",
  accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID,
  secretKey: process.env.LOCKWELL_SECRET_KEY,
});

const body = "hello world";
const res = await native.putObject("reports", "q1/summary.txt", body, {
  checksums: { crc64nvme: computeChecksumBase64("CRC64NVME", body) },
});
console.log(res.checksums.crc64nvme); // the server-verified base64 digest
go
import (
    "crypto/sha256"
    "encoding/base64"

    "github.com/lockwell/lockwell/pkg/lockwellnative"
)

native, err := lockwellnative.New("https://objects.example.com",
    os.Getenv("LOCKWELL_ACCESS_KEY_ID"), os.Getenv("LOCKWELL_SECRET_KEY"))

sum := sha256.Sum256([]byte("hello world"))
res, err := native.PutObject(ctx, lockwellnative.PutObjectInput{
    Bucket: "reports", Key: "q1/summary.txt",
    Body:      strings.NewReader("hello world"),
    Checksums: map[string]string{"sha256": base64.StdEncoding.EncodeToString(sum[:])},
})
if err != nil {
    return err
}
fmt.Println(res.Checksums["sha256"]) // the server-verified base64 digest
java
import com.lockwell.sdk.nativeapi.NativeTypes.PutOptions;
import com.lockwell.sdk.Checksums; // reuse the S3 SDK's hashing helper

byte[] body = "hello world".getBytes();
var res = native.putObject("reports", "q1/summary.txt", body,
    new PutOptions().checksum("crc64nvme", Checksums.computeBase64("CRC64NVME", body)));
System.out.println(res.checksums().get("crc64nvme")); // the server-verified digest

The S3 client takes a checksum algorithm as a put option and hashes the buffered body for you, sending x-amz-checksum-<alg>:

ts
const res = await s3.putObject("reports", "q1/summary.txt", "hello world", {
  checksumAlgorithm: "CRC64NVME",
});
console.log(res.checksums.crc64nvme); // the server-verified base64 digest
go
res, err := s3.PutObject(ctx, "reports", "q1/summary.txt", []byte("hello world"),
    lockwellsdk.WithChecksumAlgorithm(lockwellsdk.ChecksumCRC64NVME))
if err != nil {
    return err
}
fmt.Println(res.Checksums.CRC64NVME) // the server-verified base64 digest
java
import com.lockwell.sdk.LockwellClient.PutOptions;

var res = s3.putObject("reports", "q1/summary.txt", "hello world".getBytes(),
    new PutOptions().checksum("CRC64NVME"));
System.out.println(res.checksums().crc64nvme()); // the server-verified base64 digest

If the body is corrupted in transit, the server's recomputed digest will not match what was sent and the write fails with a BadDigest error (status 400). The object is never written.

Compute a digest without uploading

The SDKs export the hashing helpers, so you can precompute a digest (for a manifest, a dedup probe, or to compare against a returned value) without a network call. The output is byte-for-byte identical to what the server stores, and it feeds the native checksums map directly.

ts
import { computeChecksumBase64, checksumHeaderName, CHECKSUM_ALGORITHMS } from "@kelphect/sdk";

const digest = computeChecksumBase64("SHA256", "hello world");
const header = checksumHeaderName("SHA256"); // "x-amz-checksum-sha256"
console.log(CHECKSUM_ALGORITHMS); // ['CRC32','CRC32C','CRC64NVME','SHA1','SHA256']
java
import com.lockwell.sdk.Checksums;

String digest = Checksums.computeBase64("SHA256", "hello world".getBytes());
String header = Checksums.headerName("SHA256"); // "x-amz-checksum-sha256"
String[] all = Checksums.ALGORITHMS;

The Go SDK computes and sends the digest internally when you pass WithChecksumAlgorithm on the S3 client, so there is no exported helper to call by hand on that path. On the native path, compute the digest with the standard library (as in the Go example above) and pass it in the Checksums map.

The Checksums response shape

Native object reads surface the recorded digests as X-Lockwell-Checksum-<alg> headers, parsed into a Checksums map keyed by lowercase algorithm. Read the entry for the algorithm the object was written with.

ts
const got = await native.getObject("reports", "q1/summary.txt");
if (got.checksums.crc64nvme) {
  // re-verify downstream if you want to: hash got.body yourself and compare
}
go
got, err := native.GetObject(ctx, lockwellnative.GetObjectInput{Bucket: "reports", Key: "q1/summary.txt"})
defer got.Close()
fmt.Println(got.Checksums["crc64nvme"]) // keyed by lowercase algorithm
java
try (var got = native.getObject("reports", "q1/summary.txt")) {
    System.out.println(got.checksums().get("crc64nvme"));
}

The S3 client returns a Checksums value with one field per algorithm. Only the field for the algorithm the object was written with is populated; the rest are empty strings. The value is base64-encoded, matching the x-amz-checksum-<alg> wire value.

Field (Go / Java)Field (Node)Wire header
CRC32crc32x-amz-checksum-crc32
CRC32Ccrc32cx-amz-checksum-crc32c
CRC64NVMEcrc64nvmex-amz-checksum-crc64nvme
SHA1sha1x-amz-checksum-sha1
SHA256sha256x-amz-checksum-sha256

PutObject, GetObject, HeadObject, UploadPart, and CompleteMultipartUpload all carry a Checksums value on the S3 client.

ts
const got = await s3.getObject("reports", "q1/summary.txt");
if (got.checksums.crc64nvme) {
  // re-verify downstream if you want to
}

Per-part checksums on multipart uploads

A multipart upload checksums each part on the way up, then folds the parts into one composite checksum on the assembled object. On the native path, supply the per-part digest in the checksums map on every uploadPart, and read the composite off the object after completion.

ts
const mpu = await native.createMultipartUpload("reports", "big.bin", {
  contentType: "application/octet-stream",
});

const parts = [];
for (let n = 1; n <= chunks.length; n++) {
  const p = await native.uploadPart("reports", "big.bin", mpu.uploadId, n, chunks[n - 1], {
    checksums: { crc32c: computeChecksumBase64("CRC32C", chunks[n - 1]) },
  });
  parts.push({ partNumber: n, etag: p.etag });
}

const done = await native.completeMultipartUpload("reports", "big.bin", mpu.uploadId, parts);
const head = await native.headObject("reports", "big.bin");
console.log(head.checksums.crc32c); // the composite checksum of the whole object
go
mpu, err := native.CreateMultipartUpload(ctx, "reports", "big.bin")

for n, chunk := range chunks {
    // each part carries its own X-Lockwell-Checksum-<alg> digest server-side
    _, err := native.UploadPart(ctx, lockwellnative.UploadPartInput{
        Bucket: "reports", Key: "big.bin", UploadID: mpu.UploadID,
        PartNumber: n + 1, Body: bytes.NewReader(chunk),
    })
    if err != nil {
        return err
    }
}

_, err = native.CompleteMultipartUpload(ctx, lockwellnative.CompleteMultipartInput{
    Bucket: "reports", Key: "big.bin", UploadID: mpu.UploadID,
})
head, err := native.HeadObject(ctx, lockwellnative.GetObjectInput{Bucket: "reports", Key: "big.bin"})
fmt.Println(head.Checksums["crc32c"]) // composite over all parts
java
var mpu = native.createMultipartUpload("reports", "big.bin", "application/octet-stream");

var etags = new java.util.ArrayList<String>();
for (int n = 0; n < chunks.size(); n++) {
    var p = native.uploadPart("reports", "big.bin", mpu.uploadId(), n + 1, chunks.get(n));
    etags.add(p.etag());
}

native.completeMultipartUpload("reports", "big.bin", mpu.uploadId());
var head = native.headObject("reports", "big.bin");
System.out.println(head.checksums().get("crc32c")); // composite checksum

The S3 client declares the algorithm at CreateMultipartUpload, echoes it back, and you pass the echoed algorithm to every UploadPart. The composite lands on CompleteMultipartUpload:

ts
const mpu = await s3.createMultipartUpload("reports", "big.bin", {
  checksumAlgorithm: "CRC32C",
});

const parts = [];
for (let n = 1; n <= chunks.length; n++) {
  const p = await s3.uploadPart("reports", "big.bin", mpu.uploadId, n, chunks[n - 1], {
    checksumAlgorithm: mpu.checksumAlgorithm, // each part carries a verified digest
  });
  parts.push({ partNumber: n, etag: p.etag });
}

const done = await s3.completeMultipartUpload("reports", "big.bin", mpu.uploadId, parts);
console.log(done.checksums.crc32c); // the composite checksum of the whole object
go
mpu, err := s3.CreateMultipartUpload(ctx, "reports", "big.bin",
    lockwellsdk.WithChecksumAlgorithm(lockwellsdk.ChecksumCRC32C))

var parts []lockwellsdk.CompletedPart
for n, chunk := range chunks {
    p, err := s3.UploadPart(ctx, "reports", "big.bin", mpu.UploadID, n+1, chunk,
        lockwellsdk.WithPartChecksum(mpu.ChecksumAlgorithm))
    if err != nil {
        return err
    }
    parts = append(parts, lockwellsdk.CompletedPart{PartNumber: n + 1, ETag: p.ETag})
}

done, err := s3.CompleteMultipartUpload(ctx, "reports", "big.bin", mpu.UploadID, parts)
fmt.Println(done.Checksums.CRC32C) // the composite checksum of the whole object
java
var mpu = s3.createMultipartUpload("reports", "big.bin", "application/octet-stream", "CRC32C");

var etags = new java.util.ArrayList<String>();
for (int n = 0; n < chunks.size(); n++) {
    var p = s3.uploadPart("reports", "big.bin", mpu.uploadId(), n + 1,
        chunks.get(n), mpu.checksumAlgorithm()); // verified per-part digest
    etags.add(p.etag());
}

var done = s3.completeMultipartUpload("reports", "big.bin", mpu.uploadId(),
    null, etags, null);
System.out.println(done.checksums().crc32c()); // the composite checksum

The per-part digest is sent as a direct x-amz-checksum-<alg> header (S3) or X-Lockwell-Checksum-<alg> header (native) on each part, so each part is validated end to end and folded into the composite. See multipart uploads for the full lifecycle.

The WebCrypto SHA-256 helper for edge runtimes

The Node S3 client and the hashing helpers in checksum.js import node:crypto, so they do not bundle for Cloudflare Workers, Vercel Edge, Bun, or Deno. The edge-safe entry point exports a single helper, sha256ChecksumBase64, built on the web-standard crypto.subtle so it runs with no Node dependency. It pairs with the native client, which is edge-compatible.

On the edge, reach for @kelphect/sdk/edge and the native client. The S3 client and the full hashing helpers

depend on node:crypto and will not bundle there. :::

ts
import { NativeClient, sha256ChecksumBase64 } from "@kelphect/sdk/edge";

const native = new NativeClient({
  endpoint: env.LOCKWELL_ENDPOINT,
  accessKeyId: env.LOCKWELL_ACCESS_KEY_ID,
  secretKey: env.LOCKWELL_SECRET_KEY,
});

const body = "hello from the edge";
await native.putObject("reports", "edge.txt", body, {
  checksums: { sha256: await sha256ChecksumBase64(body) },
});

sha256ChecksumBase64 accepts a string, Uint8Array, or ArrayBuffer and returns the base64 digest as a promise. It covers the common SHA-256 case on the web path. For CRC32C or CRC64NVME on the edge, precompute the digest and pass it as a string. See edge runtimes for what else is edge-safe.

When to use which

NeedAlgorithm
Default, fast, full-object integrityCRC64NVME
Cheapest CRC, broad tooling parityCRC32
CRC with hardware acceleration on most CPUsCRC32C
Cryptographic digest (attestation, content addressing, regulated data)SHA256
Legacy SHA1 parity with an external systemSHA1

Checksums pair naturally with idempotency on the native path: a streaming PUT is never buffered, so the server uses a supplied checksum to confirm a retried write is the same payload. See conditional writes and idempotency.

Released under the Apache-2.0 License. License