Skip to content

Commit 84e2c82

Browse files
committed
new version
1 parent 955a36f commit 84e2c82

17 files changed

+3343
-501
lines changed

.github/workflows/verify.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: verify
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
schedule:
8+
- cron: "0 3 * * *"
9+
10+
jobs:
11+
verify:
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os:
16+
- ubuntu-latest
17+
- macos-latest
18+
runs-on: ${{ matrix.os }}
19+
env:
20+
OPENSSL_VERSION: "3.5.0"
21+
OPENSSL_PREFIX: ${{ runner.temp }}/openssl-3.5.0
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Install Linux build tools
26+
if: runner.os == 'Linux'
27+
run: |
28+
sudo apt-get update
29+
sudo apt-get install -y perl
30+
31+
- name: Cache OpenSSL
32+
id: cache-openssl
33+
uses: actions/cache@v4
34+
with:
35+
path: ${{ env.OPENSSL_PREFIX }}
36+
key: openssl-${{ env.OPENSSL_VERSION }}-${{ runner.os }}-${{ runner.arch }}
37+
38+
- name: Build OpenSSL
39+
if: steps.cache-openssl.outputs.cache-hit != 'true'
40+
run: sh ci/build_openssl.sh
41+
42+
- name: Verify
43+
run: make clean all verify OPENSSL_PREFIX="${OPENSSL_PREFIX}"
44+
45+
- name: Portable fuzz run
46+
run: make fuzz-run FUZZ_RUNS=5000 OPENSSL_PREFIX="${OPENSSL_PREFIX}"
47+
48+
nightly-fuzz:
49+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
50+
runs-on: ubuntu-latest
51+
env:
52+
OPENSSL_VERSION: "3.5.0"
53+
OPENSSL_PREFIX: ${{ runner.temp }}/openssl-3.5.0
54+
steps:
55+
- uses: actions/checkout@v4
56+
57+
- name: Install Linux build tools
58+
run: |
59+
sudo apt-get update
60+
sudo apt-get install -y perl
61+
62+
- name: Cache OpenSSL
63+
id: cache-openssl
64+
uses: actions/cache@v4
65+
with:
66+
path: ${{ env.OPENSSL_PREFIX }}
67+
key: openssl-${{ env.OPENSSL_VERSION }}-${{ runner.os }}-${{ runner.arch }}
68+
69+
- name: Build OpenSSL
70+
if: steps.cache-openssl.outputs.cache-hit != 'true'
71+
run: sh ci/build_openssl.sh
72+
73+
- name: Nightly verify
74+
run: make clean all verify OPENSSL_PREFIX="${OPENSSL_PREFIX}"
75+
76+
- name: Extended fuzz run
77+
run: make fuzz-run FUZZ_RUNS=20000 OPENSSL_PREFIX="${OPENSSL_PREFIX}"

README.md

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,33 @@ eed is a simple, minimal, secure text editor inspired by ed, written in C.
44

55
✅ Supports encrypted text files
66

7-
✅ Uses AES-256-CBC for encryption
7+
✅ Uses Argon2id for password-based key derivation
88

9-
✅ Uses HMAC-SHA-256 for integrity protection (MAC)
9+
✅ Uses AES-256-GCM for authenticated encryption
1010

11-
✅ Compatible with OpenSSL 3.x → no deprecated functions
11+
✅ Compatible with OpenSSL 3.5+ for Argon2id + AEAD primitives
1212

13-
Does not leak plaintext to disk
13+
Save path writes ciphertext only
1414

15-
Holds buffer in RAM only
15+
Keeps the working buffer in process memory, with optional strict memory-lock enforcement
1616

1717
✅ Works on Linux, macOS
1818

1919
Features
2020

2121
Password protected: securely prompts for password (hidden input)
2222

23-
PBKDF2 (SHA-256) for key derivation (easy to swap to Argon2 if needed)
23+
Fixed Argon2id profile for password-based key derivation
2424

25-
AES-256-CBC encryption
25+
AES-256-GCM authenticated encryption
2626

27-
HMAC-SHA-256 message authentication (MAC) → detects tampering
27+
Authenticated `EED4` file format with header-bound associated data
2828

29-
No temp files, only in-memory buffer until explicitly written
29+
Atomic encrypted saves with same-directory temp files (ciphertext only)
3030

31-
Compatible with OpenSSL 3.x API → uses EVP_MAC, no deprecated HMAC_CTX
31+
Compatible with OpenSSL 3.5+ API → uses `EVP_KDF` for Argon2id and `EVP_CIPHER` AEAD APIs
32+
33+
Encrypted recovery snapshots after each mutating command and mirrored backup copies for the latest durable state
3234

3335
Simple ed-like commands:
3436

@@ -49,18 +51,64 @@ Simple ed-like commands:
4951

5052
Security Notes
5153

52-
Editor uses AES-256-CBC with random IV per file
54+
Editor uses AES-256-GCM with a random 96-bit nonce per file
55+
56+
Uses an authenticated `EED4` header and currently accepts only the fixed supported Argon2id profile
57+
58+
Authenticates the full header as AEAD associated data before any plaintext is accepted
59+
60+
If AEAD authentication fails, the file is not opened
5361

54-
Uses PBKDF2 with 100,000 iterations (easy to increase or switch to Argon2)
62+
Saves are atomic: encrypted output is written to a temp file, synced, then renamed over the target
5563

56-
Adds HMAC-SHA-256 over IV + ciphertext → integrity check
64+
Each open file is held with an exclusive advisory lock, and saves refuse to proceed if the pathname no longer points at the file originally opened
5765

58-
If MAC verification fails, the file is not opened
66+
The editor save path intentionally writes only ciphertext; swap, hibernation, snapshots, and backups remain outside the editor's control
5967

60-
No plaintext is written to disk unless you explicitly write
68+
Every mutating command refreshes an encrypted `.recovery` snapshot, and each save refreshes an encrypted `.bak` mirror before the primary rename so crashes still leave a recoverable ciphertext copy
69+
70+
Quit will attempt to refresh a final encrypted recovery snapshot before exiting if the in-memory buffer is dirty and the prior recovery snapshot is stale
71+
72+
The parent directory must be owned by the current user and not writable by group or others
6173

6274
Plaintext buffer and password are securely wiped on exit
6375

76+
Memory locking is attempted at startup; set `EED_REQUIRE_MLOCK=1` to fail closed on hosts where `mlockall()` is unavailable
77+
78+
Current release writes an `EED4` file format and does not open files written by older releases
79+
80+
81+
Verification
82+
83+
`make` builds the release binary
84+
85+
`make asan` builds the sanitizer-enabled regression binary
86+
87+
`make verify` runs static analysis, PTY smoke testing, property-style round-trip testing, deterministic vector checks, and malformed-file regression sweeps
88+
89+
`make recovery` runs crash-recovery and latest-backup regression coverage
90+
91+
`make fuzz-build` builds a portable sanitizer-backed fuzz harness for `load_encrypted()`
92+
93+
`make libfuzzer-build` attempts the libFuzzer variant and prints guidance when the local toolchain does not provide the runtime
94+
95+
`make audit-manifest` writes `dist/sha256.txt` and `dist/build-info.txt` for exact-build review
96+
97+
98+
Assurance Docs
99+
100+
`docs/threat-model.md` defines the threat model and explicit non-goals
101+
102+
`docs/security-invariants.md` records the invariants the code is expected to preserve
103+
104+
`docs/storage-model.md` explains what the save path can and cannot honestly claim about recoverability
105+
106+
`docs/release-process.md` documents the exact-build audit and signed-release workflow
107+
108+
`docs/file-format-eed4.md` freezes the binary format and includes a deterministic test vector
109+
110+
`docs/c-hardening-roadmap.md` lays out the next hardening steps within a C-only codebase
111+
64112

65113
Limitations
66114

@@ -72,4 +120,6 @@ No multi-level undo
72120

73121
No support for !shell commands (on purpose → security)
74122

75-
No auto-recovery (intentional → secure)
123+
Crash recovery is sidecar-based; there is no multi-version recovery browser or undo journal
124+
125+
Does not support symbolic links or files with multiple hard links

ci/build_openssl.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
OPENSSL_VERSION="${OPENSSL_VERSION:-3.5.0}"
5+
OPENSSL_PREFIX="${OPENSSL_PREFIX:-$PWD/.openssl/$OPENSSL_VERSION}"
6+
7+
if [ -x "$OPENSSL_PREFIX/bin/openssl" ]; then
8+
exit 0
9+
fi
10+
11+
BUILD_ROOT="$(mktemp -d)"
12+
ARCHIVE="$BUILD_ROOT/openssl-$OPENSSL_VERSION.tar.gz"
13+
SOURCE_DIR="$BUILD_ROOT/openssl-$OPENSSL_VERSION"
14+
15+
cleanup() {
16+
rm -rf "$BUILD_ROOT"
17+
}
18+
trap cleanup EXIT
19+
20+
curl -fsSL \
21+
-o "$ARCHIVE" \
22+
"https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VERSION/openssl-$OPENSSL_VERSION.tar.gz"
23+
tar -xzf "$ARCHIVE" -C "$BUILD_ROOT"
24+
25+
cd "$SOURCE_DIR"
26+
./config --prefix="$OPENSSL_PREFIX" --openssldir="$OPENSSL_PREFIX/ssl"
27+
make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)"
28+
make install_sw

docs/c-hardening-roadmap.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# C Hardening Roadmap
2+
3+
## Current State
4+
5+
The editor remains a C codebase by design. The current hardening strategy is therefore to narrow the dangerous surface, freeze the crypto container, and keep verification pressure high rather than to promise language-level memory safety.
6+
7+
## Immediate Controls
8+
9+
- Keep the crypto format fixed and documented in `docs/file-format-eed4.md`.
10+
- Keep `make verify` mandatory before release.
11+
- Keep `make fuzz-run` in regular use against `load_encrypted()`.
12+
- Preserve explicit cleansing of passwords, keys, tags, and plaintext buffers.
13+
- Keep risky helper functions `static` and continue preferring fixed-size buffers and explicit bounds checks.
14+
15+
## Near-Term Assurance Work
16+
17+
- Add stricter warning gates such as `-Wconversion`, `-Wshadow`, and `-Wstrict-prototypes` once the tree is clean under them.
18+
- Run `clang-tidy` and `cppcheck` in CI as advisory jobs.
19+
- Perform a manual MISRA-C-oriented review of the parser and save path, while being explicit that the project is not yet claiming formal MISRA compliance.
20+
- Expand deterministic test vectors to cover empty files and maximum-length line edge cases.
21+
22+
## Higher-Assurance Options Without Leaving C
23+
24+
- Split the file-format parser and serializer into a smaller translation unit with a narrower API.
25+
- Add CBMC or Frama-C proofs for header parsing, bounds checks, and line-count safety properties.
26+
- Add negative tests for every rejected header field combination in `EED4`.
27+
- Consider a dedicated hardened build profile with stack canaries, full RELRO, and platform-specific linker hardening flags.
28+
29+
## Explicit Non-Claim
30+
31+
This roadmap improves assurance inside a C codebase. It does not make C memory-safe, and it should not be described that way.

docs/file-format-eed4.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# EED4 File Format
2+
3+
## Fixed Crypto Profile
4+
5+
- KDF: Argon2id
6+
- Argon2 version: 19
7+
- Memory cost: 65536 KiB
8+
- Iterations: 3
9+
- Lanes: 1
10+
- Cipher: AES-256-GCM
11+
- Nonce length: 12 bytes
12+
- Tag length: 16 bytes
13+
- Header authentication: the full 48-byte header is passed as AEAD associated data
14+
15+
## Plaintext Encoding
16+
17+
The editor serializes the in-memory buffer as a byte stream of newline-terminated lines. Each stored line is emitted exactly as typed, followed by `0x0a`. An empty editor buffer produces an empty plaintext stream.
18+
19+
## Binary Layout
20+
21+
| Offset | Length | Field |
22+
| ------ | ------ | ----- |
23+
| `0` | `4` | Magic ASCII `EED4` |
24+
| `4` | `1` | File format version (`0x01`) |
25+
| `5` | `1` | KDF identifier (`0x01` = Argon2id) |
26+
| `6` | `1` | Cipher identifier (`0x01` = AES-256-GCM) |
27+
| `7` | `1` | Flags (`0x00`) |
28+
| `8` | `4` | Argon2 memory cost in KiB, big-endian |
29+
| `12` | `4` | Argon2 iteration count, big-endian |
30+
| `16` | `4` | Argon2 lane count, big-endian |
31+
| `20` | `16` | Random salt |
32+
| `36` | `12` | Random AES-GCM nonce |
33+
| `48` | `n` | Ciphertext |
34+
| `48 + n` | `16` | AES-GCM tag |
35+
36+
## Acceptance Rules
37+
38+
- The loader only accepts the fixed profile above.
39+
- Older `EED3` files are intentionally rejected.
40+
- The loader rejects oversized payloads, NUL bytes, overlong lines, excessive line counts, and authentication failures.
41+
42+
## Deterministic Test Vector
43+
44+
This vector is fixed in `tests/test_vectors.py` and is intended to detect accidental format or primitive drift.
45+
46+
- Password: `vector-passphrase`
47+
- Salt: `000102030405060708090a0b0c0d0e0f`
48+
- Nonce: `101112131415161718191a1b`
49+
- Plaintext lines:
50+
- `alpha`
51+
- `beta/gamma`
52+
- `Line 3 with spaces`
53+
54+
Header:
55+
56+
```text
57+
4545443401010100000100000000000300000001000102030405060708090a0b0c0d0e0f101112131415161718191a1b
58+
```
59+
60+
Ciphertext:
61+
62+
```text
63+
e0e8b9f9d13cf983046e60ed522f9e1d27e5b302ee5551ff4135f8a4614a53a20324ff84
64+
```
65+
66+
Tag:
67+
68+
```text
69+
2a1c8c48936d3b5555a2e7b3d1fa2eb2
70+
```
71+
72+
Full file:
73+
74+
```text
75+
4545443401010100000100000000000300000001000102030405060708090a0b0c0d0e0f101112131415161718191a1be0e8b9f9d13cf983046e60ed522f9e1d27e5b302ee5551ff4135f8a4614a53a20324ff842a1c8c48936d3b5555a2e7b3d1fa2eb2
76+
```

docs/release-process.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Release And Audit Process
2+
3+
## Purpose
4+
5+
This repository can now produce a repeatable internal verification bundle, but an actual independent audit still requires a separate reviewer and a signing key that are not part of this workspace.
6+
7+
## Minimum Release Procedure
8+
9+
1. Build from a clean checkout of the exact commit being released.
10+
2. Set a fixed `SOURCE_DATE_EPOCH` for the release run.
11+
3. Run `make clean all verify audit-manifest`.
12+
4. Archive the resulting binary plus `dist/sha256.txt` and `dist/build-info.txt`.
13+
5. Record the exact OpenSSL version, compiler version, operating system, and committed `EED4` format spec in the release notes.
14+
15+
## External Audit Procedure
16+
17+
1. Hand the auditor the exact commit ID, the release artifact, and the audit manifest.
18+
2. Require the auditor to rebuild from the same commit and compare the resulting hashes against `dist/sha256.txt`.
19+
3. Scope the audit to the exact binary hash being shipped, not just to the source tree in general.
20+
4. Treat any source change, compiler change, or dependency change as a new audit candidate.
21+
22+
## Signed Release Procedure
23+
24+
1. Generate the audit manifest with `make audit-manifest`.
25+
2. Sign `dist/sha256.txt` and the release notes with an offline key that is kept outside the build machine.
26+
3. Publish the signature, the public verification key, and the binary hash together.
27+
4. Make signature verification part of the deployment checklist.
28+
29+
The repository does not ship a signing key, and it should not. Signed releases need an operator-controlled trust root.
30+
31+
## Reproducibility Notes
32+
33+
- Avoid embedding ad hoc local changes, generated files, or untracked patches in the release build.
34+
- Pin the compiler family and OpenSSL build used for production.
35+
- Rebuild from scratch for each release instead of reusing a previously compiled binary.
36+
- Keep the deterministic vector in `docs/file-format-eed4.md` and `tests/test_vectors.py` in sync with the shipping format.
37+
38+
## What This Repo Can Do Today
39+
40+
- Release build: `make`
41+
- Sanitized regression build: `make asan`
42+
- Static analysis: `make analyze`
43+
- PTY smoke test: `make smoke`
44+
- Property-style round-trip test: `make property`
45+
- Deterministic format/vector test: `make vectors`
46+
- Crash-recovery and backup regression test: `make recovery`
47+
- Malformed-file regression sweep: `make malformed`
48+
- Portable parser fuzz harness build: `make fuzz-build`
49+
- Optional libFuzzer build on supported toolchains: `make libfuzzer-build`
50+
The target prints guidance instead of hard-failing when the local clang does not ship the fuzzer runtime.
51+
- Audit manifest bundle: `make audit-manifest`
52+
- Continuous verify/fuzz workflow: `.github/workflows/verify.yml`
53+
54+
## What Still Requires External Work
55+
56+
- A truly independent review of the final ship binary
57+
- Real release signing with an offline organizational key
58+
- Longer-running fuzz campaigns with corpus management and crash triage

0 commit comments

Comments
 (0)