Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ migration framework) are implemented and tested. The post-quantum hybrid
sharing layer and the application cutover are tracked in
[`docs/migration-plan.md`](docs/migration-plan.md).

## Branding

DIS exports a `DIS_BRANDING` string constant for applications that want to
display a "Powered by DIS — Defensive Integration Shield" badge. This is
optional; the branding requirements live in [`docs/licensing.md`](docs/licensing.md).

```ts
import { DIS_BRANDING } from '@dis/shield';
// 'Powered by DIS — Defensive Integration Shield'
```

## Install

```bash
Expand Down
101 changes: 99 additions & 2 deletions docs/api-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,91 @@ isCurrentVaultEntryEnvelope(sealed): boolean
encryptAttachment(input: EncryptAttachmentInput): Promise<{ manifest; manifestRoot }>
decryptAttachment(input: DecryptAttachmentInput): Promise<void>

// KDF
// KDF (see @dis/shield/kdf for low-level access; vault-crypto profile below)
deriveMasterKey(password, saltBase64, opts?): Promise<CryptoKey> // == deriveAesGcmKey
deriveRawKey(password, saltBase64, opts?): Promise<Uint8Array>
generateSalt(): string

// Note: `KDF_PARAMS` is **not** exported from the SDK facade.
// The SDK re-exports `CURRENT_KDF_VERSION` and `DEFAULT_KDF_PARAMS` from
// @dis/shield/kdf. The plain-record `KDF_PARAMS` alias lives in
// @dis/shield/vault-crypto and is meant for Singra's call sites that look
// up params by version. For new applications, prefer `DEFAULT_KDF_PARAMS`
// from the SDK facade or @dis/shield/kdf.

// Two KDF paths exist — pick by what you need:
//
// (1) Generic: takes a typed `opts.strengthen` for HKDF-Expand second-factor binding
deriveAesGcmKey(password, saltBase64, {
version?: number, // KDF version registry; defaults to CURRENT_KDF_VERSION
params?: Readonly<Record<number, KdfParams>>, // optional override
strengthen?: { // optional HKDF second-factor binding
hkdfSalt: Uint8Array,
info: string, // caller-owned domain-separation label
},
}): Promise<CryptoKey>

// (2) Singra Vault profile (vault-crypto): positional `deviceKey` is the common case
deriveRawKey(masterPassword, saltBase64, kdfVersion?, deviceKey?): Promise<Uint8Array>
deriveKey(masterPassword, saltBase64, kdfVersion?, deviceKey?): Promise<CryptoKey>
deriveRawKeySecure(masterPassword, saltBase64, kdfVersion?, deviceKey?): Promise<SecureBuffer>
//
// When `deviceKey` is supplied, DIS strengthens the Argon2id output via HKDF-Expand
// with HKDF info = 'SINGRA_DEVICE_KEY_V1' and salt = the device key bytes.
// (The `info` string is part of the wire-format contract; see crypto-dependency-map.md.)

// Key management
createWrappedUserKey(kdfOutputBytes, scheme?): Promise<UserKeyBundle>
unwrapUserKey(encryptedUserKey, kdfOutputBytes, scheme?): Promise<CryptoKey>
rotateEncryptionKeys(encryptedUserKey, oldKdf, newKdf, scheme?): Promise<string>

// TOTP — the SDK facade re-exports the 2FA-enrolment surface only.
// For password-manager authenticator code (third-party imported entries
// with non-default algorithm / digits / period), import directly:
// import { generateTotpCode, buildTotpUriWithOptions } from '@dis/shield/totp'
// The SDK export intentionally omits these so the 2FA-enrolment contract
// (SHA-1 / 6 digits / 30 s) is the only thing 2FA call sites can reach.
generateTotpSecret(): string
buildTotpUri({ issuer, label, secret }): string
verifyTotpCode(secret, code, window?): boolean

// Integrity & migrations
verifyPayloadIntegrity(bytes, expectedBase64): Promise<void>
new MigrationRegistry().register(...).migrateToLatest(...)

// Migration framework — see docs/migrations.md for the full guide.
// DIS provides the framework; the application registers concrete steps.
new MigrationRegistry().register(migration: Migration).migrateToLatest(
subject: string,
payload: string,
detect: VersionDetector,
context: MigrationContext,
): Promise<string>

// ---- Two functions share a name across modules — read carefully. -----------
//
// (a) Storage-format migration for a wrapped private key.
// Lives in @dis/shield/vault-crypto. Rewrites a stored
// 'salt:enc' / 'ver:salt:enc' private-key blob to the hybrid
// 'pq-v2:ver:salt:encRsa:encPq' form.
migrateToHybridKeyPair(encryptedPrivateKey: string, masterPassword: string):
Promise<{ publicKey: string; encryptedPrivateKey: string; pqPublicKey: string } | null>

// (b) Cipher-version migration for an already-hybrid PQ+RSA ciphertext.
// Lives in @dis/shield/post-quantum. Re-encodes a v1 (0x01 RSA-only),
// legacy (0x02), or standard v1 (0x03) hybrid ciphertext into the
// current standard v2 (0x04). Byte-layout preserved.
migrateToHybrid(
legacyCiphertext: string,
rsaPrivateKey: string, // JWK string
pqSecretKey: string | null, // base64; required for 0x02 and 0x03
pqPublicKey: string, // base64
rsaPublicKey: string, // JWK string
aad?: string,
): Promise<string>
//
// Use (a) when you are reading a legacy stored private key and want to
// upgrade its storage format. Use (b) when you already have a hybrid
// ciphertext at an old version and want to refresh it in place.
```

## Design rules
Expand All @@ -56,6 +128,31 @@ new MigrationRegistry().register(...).migrateToLatest(...)
`DisLegacyPayloadError`.
6. **Providers are injectable** via `setCryptoProvider` for tests/hardware.

## Error handling

DIS throws typed errors from `@dis/shield/core`. Map them to user-facing
copy in your application; do not surface raw `message` to end users (some
include stack-trace-ish details that are not useful to a user and may
leak internal state). Do not switch on the string of the message — switch
on the `code` property or the constructor.

| Error class | `code` (when applicable) | When it fires | Suggested app behaviour |
| --- | --- | --- | --- |
| `DisDecryptionError` | — | AEAD failure: wrong key, tampered ciphertext, or wrong AAD | Treat as "wrong password" or "data corruption". Do **not** expose the cause — there is no oracle, by design. |
| `DisIntegrityError` | — | SHA-256 verification of a stored payload (manifest, hash chain) failed | Trigger quarantine / re-fetch from a trusted source / log for the security team. |
| `DisUnsupportedFormatVersionError` | — | Ciphertext carries an unknown in-family version prefix (e.g. `sv-vault-v9:`) | Refuse to read. Ask the user to update the app. |
| `DisLegacyPayloadError` | — | Runtime read hit an unversioned / no-AAD payload | Refuse on the runtime path. Re-try through the explicit migration helper (`decryptVaultEntryForMigration` etc.) or refuse. |
| `DisInvalidArgumentError` | — | Caller passed a bad argument (empty string, wrong length, missing required AAD, …) | Programming error in the app. Log and surface a generic error. |
| `DisError` | `'UNSUPPORTED_KDF_VERSION'` | The KDF version stored for an account is not in the registry | Refuse unlock. Ask the user to update (or roll back, but never silently upgrade). |
| `DisError` | `'KEY_DERIVATION_FAILED'` | Argon2id output was the wrong type | Should not happen; report as a bug. |
| `DisError` | `'INVALID_ARGUMENT'` | Migration cycle detected | The migration registry has a bug; report. |
| `DisError` | `'SECURITY_STANDARD_BLOCKED'` (post-quantum) | Hybrid ciphertext at a version not permitted by the security standard | Refuse. Run the migration helper (`migrateToHybrid`) explicitly. |

**The contract:** AEAD failures do not reveal cause (no padding/AAD oracle).
Your error UX must respect that. Log the type for yourself, but show the
user a generic "could not decrypt — wrong password or corrupted data"
message.

## Breaking-change policy

- Format-frozen constants are append-only (`sv-vault-v1` → add `sv-vault-v2`,
Expand Down
86 changes: 86 additions & 0 deletions docs/app-to-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Cross-Reference: Application → DIS Module

> Which DIS module does each application consume, and for what? This file is
> the map from a Singra product to the underlying primitive it relies on.
> Use it when debugging or when planning to swap an implementation.

## Singra Vault (`github.com/einmalmaik/singravault`)

The Vault is the most DIS-heavy application. It is **the reference
consumer**: nearly every DIS module is in use somewhere.

| Capability | Vault surface | DIS module |
| --- | --- | --- |
| Master-password KDF (Argon2id) | `deriveRawKey` / `deriveKey` | `kdf` |
| Device-key strengthening (HKDF) | `deriveRawKey(..., deviceKey)` | `kdf` |
| Vault item envelope (`sv-vault-v1:`) | `encryptVaultItem` / `decryptVaultItem` | `vault-encryption` |
| Two-tier key model (`usk-wrap-v2:`) | `createEncryptedUserKey` / `unwrapUserKey` / `rewrapUserKey` | `key-management` |
| Private-key wrapping (`usk-v1:`) | `wrapPrivateKeyWithUserKey` / `unwrapPrivateKeyWithUserKey` | `vault-crypto` |
| RSA-OAEP for emergency access | `generateRSAKeyPair` / `encryptRSA` / `decryptRSA` | `vault-crypto` (over `asymmetric`) |
| Hybrid PQ+RSA for sharing / emergency | `generateUserKeyPair(v2)` / `migrateToHybridKeyPair` | `vault-crypto` (over `post-quantum`) |
| OpLog ECDSA device signatures | `signEcdsaP256` / `verifyEcdsaP256` | `signing` |
| OpLog record/snapshot crypto | HKDF + AES-GCM | `kdf` / `aead` / `random` |
| OpLog integrity hashes | `sha256Bytes` / `sha256Hex` / `hmacSha256` | `integrity` |
| 2FA TOTP (Singra enrolment) | `verifyTotpCode` / `buildTotpUri` | `totp` |
| Password-manager authenticator codes | `generateTotpCode` / `buildTotpUriWithOptions` | `totp` (subpath) |
| HIBP password-strength (SHA-1) | `sha1Hex` | `integrity` |
| PKCE for OAuth (Web) | `sha256Bytes` / `randomBytes` | `integrity` / `random` |
| Password generator randomness | `randomInt` | `random` |
| Passkey PRF wrapping | `deriveHkdfAesGcmKey` | `kdf` |
| OPAQUE session binding | `hmacSha256` | `integrity` |
| Canonical-JSON hashing for integrity v2 | `sha256StringBase64` | `integrity` |
| Key material in memory | `SecureBuffer` | `secure-memory` |
| UUIDs in UI / orchestrators | `randomUuid` | `random` |
| Application-specific composition | `services/cryptoService.ts` is now a thin re-export of `@dis/shield/vault-crypto` | `vault-crypto` (Singra profile) |

## Singra Premium (`github.com/einmalmaik/singra-premium`)

Premium is a sibling repository that historically aliased Singra Vault's
`src/services/*` for its own crypto. As of the Phase 4 cutover it imports
DIS directly.

| Capability | Premium surface | DIS module |
| --- | --- | --- |
| Vault item encryption (delegated to UserKey) | re-uses Vault's `@dis/shield/vault-crypto` | `vault-crypto` |
| Chunked file attachments | `encryptAttachment` / `decryptAttachment` | `file-encryption` |
| File-key wrap under UserKey | via `wrapFileKey` callback that calls into `vault-crypto.encrypt` | `file-encryption` + `vault-crypto` |
| PQ hybrid keypair for shared collections | re-uses Vault's surface | `vault-crypto` (over `post-quantum`) |

Premium is the second consumer and exists to test the "two tightly-coupled
apps adopt DIS" path that the architecture decision in `architecture.md`
calls out. See `docs/migration-plan.md` Phase 4 for the cutover steps.

## A future, third application

If you are starting a new application that consumes DIS, you probably do
**not** need `@dis/shield/vault-crypto`. That module is the Singra-Vault
profile; it bakes in the `SINGRA_*` HKDF info labels, the `sv-vault-v1:`
envelope, the `usk-wrap-v2:` user-key envelope, the `pq-v2:` shared-key
envelope, and the Singra `VaultItemData` shape.

For a new app:

1. Read [`docs/integration-guide.md`](integration-guide.md) first.
2. Compose from the low-level modules: `kdf`, `aead`, `vault-encryption`,
`key-management`, `file-encryption`, `post-quantum`, `signing`, `totp`,
`integrity`, `random`, `secure-memory`, `core`.
3. Define your **own** envelope prefixes (e.g. `myapp-record-v1:`) and
**own** HKDF info labels. The contract is "pick a prefix, freeze it,
add a new version rather than editing it". See
[`docs/crypto-dependency-map.md`](crypto-dependency-map.md) for what
the format constants are for the Singra profile; your profile will
look the same, but the values are yours.

## What "Powered by DIS" means in code

If you display a "Powered by DIS — Defensive Integration Shield" badge,
the import is:

```ts
import { DIS_BRANDING } from '@dis/shield';
// DIS_BRANDING === 'Powered by DIS — Defensive Integration Shield'
```

Trademark and badge rules are in [`docs/licensing.md`](licensing.md).
Short version: non-commercial use of the badge is fine; commercial
re-branding requires a dual-license — see licensing.md.
2 changes: 1 addition & 1 deletion docs/crypto-dependency-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ time and are a baseline, not a guarantee.
| `hash-wasm` | 4.12.0 | Argon2id KDF | `singravault` |
| WebCrypto `SubtleCrypto` | platform | AES-GCM, HKDF, RSA-OAEP, SHA-256 | both |
| `@noble/post-quantum` | 0.5.4 | ML-KEM-768 (hybrid) | `singravault` |
| `otpauth` | ^9.5.0 | TOTP (out of DIS scope) | `singravault` |
| `otpauth` | ^9.5.0 | TOTP (RFC 6238) — now lives inside DIS `@dis/shield/totp` | DIS repo (dep) |

## Singra Vault — core crypto modules

Expand Down
Loading
Loading