# 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.

:::: native

::: code-group

```ts [Node]
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 [Go]
import (
    "crypto/sha256"
    "encoding/base64"

    "github.com/RusticStack/lockwell-sdk-go/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 [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 = nativeClient.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
```

:::

::::

:::: s3

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

::: code-group

```ts [Node]
const res = await s3.putObject("reports", "q1/summary.txt", "hello world", {
  checksumAlgorithm: "CRC64NVME",
});
console.log(res.checksums.crc64nvme); // the server-verified base64 digest
```

```go [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 [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.

::: code-group

```ts [Node]
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 [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.

:::: native

::: code-group

```ts [Node]
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 [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 [Java]
try (var got = nativeClient.getObject("reports", "q1/summary.txt")) {
    System.out.println(got.checksums().get("crc64nvme"));
}
```

:::

::::

:::: s3

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                |
| ----------------- | ------------ | -------------------------- |
| `CRC32`           | `crc32`      | `x-amz-checksum-crc32`     |
| `CRC32C`          | `crc32c`     | `x-amz-checksum-crc32c`    |
| `CRC64NVME`       | `crc64nvme`  | `x-amz-checksum-crc64nvme` |
| `SHA1`            | `sha1`       | `x-amz-checksum-sha1`      |
| `SHA256`          | `sha256`     | `x-amz-checksum-sha256`    |

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

```ts [Node]
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.

:::: native

::: code-group

```ts [Node]
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 [Go]
mpu, err := native.CreateMultipartUpload(ctx, "reports", "big.bin")
var parts []lockwellnative.CompleteMultipartPart

for n, chunk := range chunks {
    // each part carries its own X-Lockwell-Checksum-<alg> digest server-side
    uploaded, 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
    }
    parts = append(parts, lockwellnative.CompleteMultipartPart{PartNumber: uploaded.PartNumber, ETag: uploaded.ETag})
}

_, err = native.CompleteMultipartUpload(ctx, lockwellnative.CompleteMultipartInput{
    Bucket: "reports", Key: "big.bin", UploadID: mpu.UploadID,
    Parts: parts,
})
head, err := native.HeadObject(ctx, lockwellnative.GetObjectInput{Bucket: "reports", Key: "big.bin"})
fmt.Println(head.Checksums["crc32c"]) // composite over all parts
```

```java [Java]
var mpu = nativeClient.createMultipartUpload("reports", "big.bin", "application/octet-stream");

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

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

:::

::::

:::: s3

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`:

::: code-group

```ts [Node]
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 [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 [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](/guide/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.

::: tip 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 [Node, edge runtime]
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](/guide/edge-runtimes) for what else is edge-safe.

## When to use which

| Need                                                                   | Algorithm   |
| ---------------------------------------------------------------------- | ----------- |
| Default, fast, full-object integrity                                   | `CRC64NVME` |
| Cheapest CRC, broad tooling parity                                     | `CRC32`     |
| CRC with hardware acceleration on most CPUs                            | `CRC32C`    |
| Cryptographic digest (attestation, content addressing, regulated data) | `SHA256`    |
| Legacy `SHA1` parity with an external system                           | `SHA1`      |

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](/guide/conditional-writes).

## Related

- [Multipart uploads](/guide/multipart-uploads) for per-part and composite checksums.
- [Errors and retries](/guide/errors-and-retries) for the `BadDigest` failure path.
- [S3 operations reference](/reference/s3-operations) for the full operation matrix.
