Skip to content
Open
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
52 changes: 52 additions & 0 deletions .agents/skills/cryptography/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: cryptography
description: Use whenever generating or reviewing F# code that opens Feather.Cryptography.Encode/Hash/Cryptography or calls Base64.encodeString, Base64.toBase64Url, SHA256.sha256Hex, Crc32.crc32OfString, RSA256.createKeyPair, AES256GCM.encrypt/decrypt, EncryptedEnvelope.encrypt/decrypt, Symmetric.encrypt/decrypt, or Bcrypt.hashPassword/verifyPassword. Trigger also on mentions of envelope encryption, DEK/KEK, AAD, base64url for JWTs, AES-256-GCM, RSA-OAEP, HKDF-derived symmetric secrets, or password hashing in F#.
---

# Cryptography

Library: [FeatherTools/cryptography](https://github.com/FeatherTools/cryptography)
NuGet: `Feather.Cryptography`

## Purpose

F# library of ready-made use-cases for encryption, decryption, hashing, and encoding. It wraps the .NET cryptographic primitives behind small qualified-access modules and single-case discriminated-union types so calling code passes well-typed keys, nonces, tags, and ciphertext instead of bare byte arrays.

## When to Use

- Encoding bytes/strings to Base64 or URL-safe base64url (e.g. for JWTs or query parameters).
- Hashing strings with SHA-256 or computing a CRC-32 checksum.
- Encrypting/decrypting with RSA (OAEP-SHA256), AES-256-GCM, envelope encryption (DEK wrapped by an external KEK service), or a single-secret symmetric helper.
- Hashing and verifying passwords with bcrypt.

## When NOT to Use

- As a general-purpose crypto framework: only the use-cases below are exposed.
- For integrity-only checks where a non-cryptographic checksum is wanted security-wise — `Crc32` is a checksum, not a secure hash.
- For key/secret storage or rotation: the library encrypts but does not persist or manage key material.

## Main Concepts

- `Encode` — UTF-8 string/byte conversion and Base64 (`Base64` submodule, including base64url helpers).
- `Hash` — `SHA256` (cryptographic hash, lowercase hex) and `Crc32` (checksum over ASCII bytes).
- `Cryptography` — the encryption module hosting the types and algorithm submodules below.
- `RSA256` — asymmetric RSA-2048 key-pair generation and OAEP-SHA256 encrypt/decrypt.
- `AES256GCM` — authenticated symmetric encryption producing `IV`, ciphertext, and `Tag`.
- `EncryptedEnvelope` — record bundling a payload encrypted by a per-message `DEK`, where the `DEK` itself is wrapped by a caller-supplied function (external KEK/vault).
- `Symmetric` — single-secret encrypt/decrypt where any string secret is run through HKDF-SHA256.
- `Bcrypt` — password hashing and verification.
- `Secret` — random base64url/byte secret generation.
- Wrapper types: `PrivateKey`, `PublicKey`, `EncryptedData`, `KEK`, `DEK`, `IV`, `Tag`, `AAD`, `Version`, `Algorithm`.

## Related Libraries

- `Feather.ErrorHandling` — supplies `AsyncResult`, the `asyncResult` computation expression, and `Result.orFail`; envelope APIs return `AsyncResult<_, string>`.
- `BCrypt.Net-Core` — backs the `Bcrypt` module.

## Keywords for Search

cryptography, encryption, decryption, hashing, F#, fsharp, Feather.Cryptography, Base64, base64url, JWT, SHA256, sha256Hex, CRC32, crc32OfString, RSA, RSA256, OAEP, AES, AES256GCM, AES-256-GCM, GCM, nonce, IV, Tag, AAD, DEK, KEK, envelope encryption, EncryptedEnvelope, Symmetric, HKDF, secret, generateSecret, Bcrypt, password hashing, AsyncResult

## Reference Files

For composition principles and recommended API usage, read `references/preferred-patterns.md`. For known pitfalls and incorrect assumptions, read `references/anti-patterns.md`. For worked code examples, read `references/examples.md`.
37 changes: 37 additions & 0 deletions .agents/skills/cryptography/references/anti-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Anti-Patterns

Each item is **mistake → why → fix**. Code lives only in `references/examples.md`.

## Common Mistakes

- **Using `Crc32` to protect or fingerprint data securely** → CRC-32 is a non-cryptographic checksum for detecting accidental corruption; it is trivially forgeable → use `SHA256.sha256Hex`/`sha256` when integrity must resist tampering.

- **Expecting `SHA256.sha256Hex` to return uppercase hex** → it lowercases the output → compare against lowercase expected values (e.g. `b94d27b9...`), or normalise both sides before comparing.

- **Assuming `Crc32.crc32OfString` encodes the input as UTF-8** → it encodes via ASCII before hashing → for non-ASCII input convert bytes yourself and call `Crc32.crc32`, and never assume the checksum matches a UTF-8 CRC from another tool.

- **Treating standard Base64 and base64url as interchangeable** → JWTs and URLs require base64url (`+/=` replaced/stripped); plain `Base64.encode` output breaks them → pipe through `Base64.toBase64Url` for transport and `Base64.fromBase64Url` before `Base64.decode`.

- **Discarding the `IV` and `Tag` returned by `AES256GCM.encrypt`** → decryption needs the exact nonce and authentication tag; without them the ciphertext is unrecoverable → store/transmit all three (`IV`, ciphertext, `Tag`) together.

- **Reusing or hand-rolling a nonce for AES-GCM** → the library already generates a fresh random nonce per `encrypt` call, and nonce reuse with the same key destroys GCM security → call `AES256GCM.encrypt` again for each message and keep the returned `IV`; never inject your own.

- **Mismatching `AAD` between encrypt and decrypt** → GCM authenticates the `AAD`, so any difference (including `Some` vs `None`) throws/fails authentication → pass the identical `AAD` value to both sides.

- **Ignoring the `Result` from `Symmetric.decrypt`** → it returns `Result<string, string>` and reports wrong-secret/corrupted/format errors as `Error`, not exceptions → match on `Ok`/`Error` rather than assuming success or wrapping in `try/with`.

- **Catching exceptions around `EncryptedEnvelope.decrypt`** → it already converts `CryptographicException` into `Error "Envelope authentication failed"` on the `AsyncResult` error channel → handle the error channel; a `try/with` will never fire for auth failure.

- **Hashing passwords with `SHA256`** → fast hashes are unsuitable for passwords (no work factor, brute-forceable) → use `Bcrypt.hashPassword`/`Bcrypt.verifyPassword`.

## Do Not Use / Avoid

- **`open`-ing the qualified submodules to shorten calls** → the modules are `[<RequireQualifiedAccess>]` for disambiguation (`SHA256`, `Base64`, etc. collide with .NET names) → keep the module prefix.

- **Passing raw `byte[]` where a wrapper type is expected** → functions take `DEK`, `IV`, `Tag`, `AAD`, `PublicKey`, etc., and the compiler will reject bare arrays → wrap explicitly (`DEK bytes`, `AAD bytes`).

- **Embedding a master/KEK key inside the encrypt/decrypt callbacks naively** → the envelope design deliberately delegates DEK wrapping to your `encryptDEK`/`decryptDEK` so the master key can stay in an external service → route DEK wrapping through that external key service rather than holding the KEK in process.

## Wrong Abstractions

- **Building your own envelope by manually combining `RSA256` + `AES256GCM`** → `EncryptedEnvelope` already generates the per-message `DEK`, encrypts the payload, wraps the `DEK`, and clears key bytes from memory → use `EncryptedEnvelope.encrypt`/`decrypt` (and the batch variants for bulk).
215 changes: 215 additions & 0 deletions .agents/skills/cryptography/references/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Examples

This is the single source of all example code for the skill. Examples are ordered from basic to full workflow and are self-contained.

## Encoding

```fsharp
open Feather.Cryptography.Encode

// String <-> bytes (UTF-8)
let bytes = stringToBytes "Hello, World!"
let text = bytesToString bytes

// Standard Base64 round-trip
let encoded = Base64.encodeString "Hello, World!"
let decoded = Base64.decodeString encoded

// URL-safe base64url (for JWTs, query parameters)
let urlSafe =
"Hello, World!"
|> Base64.encodeString
|> Base64.toBase64Url

let original =
urlSafe
|> Base64.fromBase64Url
|> Base64.decodeString
```

## Hashing

```fsharp
open Feather.Cryptography.Hash

// SHA-256 as lowercase hex
let digest = SHA256.sha256Hex "hello world"
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"

// SHA-256 of raw bytes
let rawDigest = SHA256.sha256 "hello world"B

// CRC-32 checksum (hex)
let checksum = Crc32.crc32OfString "hello world"
// "d4a1185"
```

## RSA

```fsharp
open Feather.Cryptography.Cryptography

let privateKey, publicKey = RSA256.createKeyPair ()

let encrypted = RSA256.encrypt publicKey "Secret message"B
let decrypted = RSA256.decrypt privateKey encrypted
```

## AES-GCM

```fsharp
open Feather.Cryptography.Cryptography

let dek = AES256GCM.generateKey ()

// Without additional authenticated data
let iv, ciphertext, tag = AES256GCM.encrypt dek None "Sensitive data"B
let plaintext = AES256GCM.decrypt dek iv ciphertext tag None

// With AAD (authenticated, not encrypted) — must match on decrypt
let aad = AAD "record-metadata"B
let iv2, ciphertext2, tag2 = AES256GCM.encrypt dek (Some aad) "Sensitive data"B
let plaintext2 = AES256GCM.decrypt dek iv2 ciphertext2 tag2 (Some aad)
```

## Symmetric

```fsharp
open Feather.Cryptography.Cryptography

// Generate a strong secret, or supply any string from an external secret store
let secret = Symmetric.generateSecret ()

let ciphertext = Symmetric.encrypt secret "Hello, symmetric world!"

match Symmetric.decrypt secret ciphertext with
| Ok plaintext -> printfn "Decrypted: %s" plaintext
| Error msg -> printfn "Failed: %s" msg
```

## Bcrypt

```fsharp
open Feather.Cryptography.Cryptography

let hashed = Bcrypt.hashPassword "StrongPassword123!"

let isValid = Bcrypt.verifyPassword "StrongPassword123!" hashed // true
let isWrong = Bcrypt.verifyPassword "WrongPassword" hashed // false
```

## Envelope Encryption (Integration)

The payload is encrypted with a per-message `DEK`; the `DEK` is wrapped by a caller-supplied function so the master key can live in an external key service. Here RSA stands in for that service.

```fsharp
open Feather.Cryptography.Cryptography
open Feather.ErrorHandling

let privateKey, publicKey = RSA256.createKeyPair ()

let encryptDEK (DEK dek) = asyncResult {
return RSA256.encrypt publicKey dek
}

let decryptDEK encryptedDek = asyncResult {
let dekBytes = RSA256.decrypt privateKey encryptedDek
return DEK dekBytes
}

let aad = AAD "resource-123"B |> Some

let envelope =
"Top secret data"B
|> EncryptedEnvelope.encrypt encryptDEK aad
|> AsyncResult.runSynchronously
|> Result.orFail

let decrypted =
envelope
|> EncryptedEnvelope.decrypt decryptDEK aad
|> AsyncResult.runSynchronously
|> Result.orFail
```

## Envelope Encryption with an External Key Service (Integration)

Plug any vault/HSM client into the same `encryptDEK`/`decryptDEK` shape. `KeyService` is a neutral placeholder for such a client.

```fsharp
open Feather.Cryptography.Cryptography
open Feather.ErrorHandling

let encryptDEKWithService (keyService: KeyService) (DEK dek) = asyncResult {
try
let! ciphertext = keyService.WrapAsync dek |> Async.AwaitTask
return EncryptedData ciphertext
with ex ->
return! Error $"Key service wrap failed: {ex.Message}"
}

let decryptDEKWithService (keyService: KeyService) (EncryptedData wrappedDek) = asyncResult {
try
let! dek = keyService.UnwrapAsync wrappedDek |> Async.AwaitTask
return DEK dek
with ex ->
return! Error $"Key service unwrap failed: {ex.Message}"
}
```

## Batch Envelope Encryption (Integration)

Wrap many `DEK`s in a single round-trip to the key service; list order is preserved.

```fsharp
open Feather.Cryptography.Cryptography
open Feather.ErrorHandling

let encryptDEKs (deks: DEK list) = asyncResult {
return deks |> List.map (fun (DEK dek) -> RSA256.encrypt publicKey dek)
}

let decryptDEKs (wrapped: EncryptedData list) = asyncResult {
return wrapped |> List.map (fun ed -> DEK (RSA256.decrypt privateKey ed))
}

let payloads = [ "Item 1"B; "Item 2"B; "Item 3"B ]

let envelopes =
payloads
|> EncryptedEnvelope.encryptBatch encryptDEKs None
|> AsyncResult.runSynchronously
|> Result.orFail

let restored =
envelopes
|> EncryptedEnvelope.decryptBatch decryptDEKs None
|> AsyncResult.runSynchronously
|> Result.orFail
```

## Test

A round-trip and a negative case with Expecto.

```fsharp
open Expecto
open Feather.Cryptography.Cryptography

[<Tests>]
let symmetricTests =
testList "Symmetric" [
testCase "round-trips a payload" <| fun _ ->
let secret = Symmetric.generateSecret ()
let encrypted = Symmetric.encrypt secret "payload"
match Symmetric.decrypt secret encrypted with
| Ok decrypted -> Expect.equal decrypted "payload" "should match original"
| Error err -> failtest err

testCase "rejects a wrong secret" <| fun _ ->
let encrypted = Symmetric.encrypt (Symmetric.generateSecret ()) "payload"
match Symmetric.decrypt (Symmetric.generateSecret ()) encrypted with
| Ok _ -> failtest "expected decryption to fail"
| Error _ -> ()
]
```
46 changes: 46 additions & 0 deletions .agents/skills/cryptography/references/preferred-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Preferred Patterns

For all runnable code referenced below, see `references/examples.md`.

## Core Principles

- Every module is `[<RequireQualifiedAccess>]`; always call through the module name (`Base64.encode`, `RSA256.encrypt`, `AES256GCM.decrypt`), never `open` the submodule to drop the prefix.
- Keys, nonces, tags, ciphertext, and authenticated data are single-case DUs (`PrivateKey`, `PublicKey`, `DEK`, `IV`, `Tag`, `AAD`, `EncryptedData`). Construct and pattern-match them explicitly; do not pass raw `byte[]` where a wrapper is expected.
- Plaintext and binary payloads are `byte[]`; use the `Encode` helpers to cross the string boundary rather than calling `System.Text.Encoding` directly.
- Transmit binary output as text with base64url (`Base64.toBase64Url`) when it must survive URLs or JWTs; reverse with `Base64.fromBase64Url` before decoding.

## Recommended API Usage

- Encoding round-trips: pair `Base64.encodeString`/`decodeString`, and `Base64.toBase64Url`/`fromBase64Url`. See `examples.md` → Encoding.
- Hashing: `SHA256.sha256Hex` returns lowercase hex; `SHA256.sha256` returns raw bytes; `Crc32.crc32OfString` returns a hex checksum. See `examples.md` → Hashing.
- Asymmetric: generate once with `RSA256.createKeyPair`, then `RSA256.encrypt`/`decrypt`. See `examples.md` → RSA.
- Authenticated symmetric: `AES256GCM.generateKey` yields a `DEK`; `encrypt` returns the `IV`, ciphertext, and `Tag` you must retain for `decrypt`. See `examples.md` → AES-GCM.
- Single-secret helper: `Symmetric.encrypt`/`decrypt` take any string secret and a string payload; `Symmetric.generateSecret` produces a strong one. See `examples.md` → Symmetric.
- Passwords: `Bcrypt.hashPassword` then `Bcrypt.verifyPassword`. See `examples.md` → Bcrypt.

## Error Handling

- `RSA256` and `AES256GCM` raise on failure (a wrong key, tag, or `AAD` throws `CryptographicException`); wrap calls in `try/with` if the failure is expected.
- `Symmetric.decrypt` returns `Result<string, string>` — match on `Ok`/`Error` instead of catching exceptions; it already maps tampering and bad encoding to descriptive `Error` values.
- `EncryptedEnvelope.decrypt`/`decryptBatch` catch `CryptographicException` internally and surface it as `Error "Envelope authentication failed"`, so handle the `AsyncResult` error channel rather than exceptions.

## Composition

- `EncryptedEnvelope.encrypt` and `decrypt` are parameterised by a caller-supplied `encryptDEK: DEK -> AsyncResult<EncryptedData, string>` and `decryptDEK: EncryptedData -> AsyncResult<DEK, string>`. This is where an external KEK/vault/HSM is plugged in; the library never sees the master key.
- Use `encryptBatch`/`decryptBatch` when wrapping many `DEK`s in one round-trip to the key service; they preserve list order between inputs and outputs.
- The `AAD` argument, when supplied to `encrypt`, must be supplied identically to `decrypt`; it is authenticated but not encrypted.

## Integration with Feather.ErrorHandling

- Envelope APIs return `AsyncResult<_, string>`; build the `encryptDEK`/`decryptDEK` callbacks with the `asyncResult` computation expression.
- Run an `AsyncResult` to a value with `AsyncResult.runSynchronously` and unwrap with `Result.orFail` at the edge (tests, scripts), or keep composing within `asyncResult` in production code.

## Naming Conventions

- Bind destructured wrappers at the call site (`let (DEK dekBytes) = ...`, `let (IV nonce), ciphertext, (Tag tag) = ...`) to name the inner bytes clearly.
- Mirror the library's vocabulary in your own code: `dek`, `kek`, `iv`, `tag`, `aad`, `ciphertext`, `plaintext`.

## Testing Recommendations

- The suite uses Expecto; assert round-trips (`encrypt` then `decrypt` equals the original) and negative cases (wrong `AAD`/secret fails). See `examples.md` → Test.
- For known-answer tests, `SHA256.sha256Hex "hello world"` and `Crc32.crc32OfString "hello world"` have stable expected values shown in `examples.md` → Hashing.
1 change: 1 addition & 0 deletions .claude/skills
Loading