sefy-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ <p align="center"><img src="https://github.com/lacodda/sefy/raw/main/assets/banner.svg" alt="sefy - an inconspicuous encrypted vault" width="720"></p>
2
+
3
+ <p align="center">
4
+ <a href="https://crates.io/crates/sefy"><img src="https://img.shields.io/crates/v/sefy?style=flat-square" alt="crates.io"></a>
5
+ <a href="https://www.npmjs.com/package/sefy-cli"><img src="https://img.shields.io/npm/v/sefy-cli?style=flat-square" alt="npm"></a>
6
+ <a href="https://github.com/lacodda/sefy/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/lacodda/sefy/ci.yml?branch=main&style=flat-square" alt="CI"></a>
7
+ <a href="https://github.com/lacodda/sefy/blob/main/LICENSE"><img src="https://img.shields.io/crates/l/sefy?style=flat-square" alt="MIT"></a>
8
+ </p>
9
+
10
+ # sefy
11
+
12
+ Every way of storing secrets announces itself. A `.kdbx` file says "password
13
+ database". age and gpg write a header. VeraCrypt wants a container and a mount.
14
+ Whoever looks at your disk, your backup drive or your cloud folder can tell
15
+ exactly where the interesting file is.
16
+
17
+ **sefy is a secret store whose file looks like nothing.** Notes, credentials and
18
+ files live in an encrypted SQLite database sealed into a single blob with no
19
+ magic bytes, no header and no extension convention. Call it `notes.bak`, leave
20
+ it among your other backups, and there is nothing to notice.
21
+
22
+ ```
23
+ $ head -c 32 notes.bak | xxd
24
+ 00000000: 65b0 1375 a933 361d 89e2 9338 1b8d cb76 e..u.36....8...v
25
+ 00000010: 0749 7515 ad12 dd40 1c3e 3e93 9282 4b5a .Iu....@.>>...KZ
26
+ ```
27
+
28
+ ## Threat model, honestly
29
+
30
+ - **Protects against:** a passing glance, a curious file listing, a cloud-side
31
+ scanner looking for known formats, anyone who does not already know the file
32
+ is a vault.
33
+ - **Does not protect against:** forensic analysis - a high-entropy headerless
34
+ file is recognizable as *some* container to an examiner - or anyone who can
35
+ compel you to give up the password.
36
+
37
+ sefy is **inconspicuous, not deniable**, and it will not pretend otherwise.
38
+
39
+ ## A day with it
40
+
41
+ Point sefy at a file. There is no default location: a vault at a predictable
42
+ path would undo the whole point.
43
+
44
+ ```
45
+ $ export SEFY_VAULT=~/backups/notes.bak
46
+
47
+ $ sefy init
48
+ Master password:
49
+ created /home/you/backups/notes.bak
50
+ ```
51
+
52
+ Put things in. Notes, logins, and files kept byte for byte.
53
+
54
+ ```
55
+ $ sefy add note "bank card" --text "PIN 4815" --tag money
56
+ added "bank card" as 1
57
+
58
+ $ sefy add credential mail --login someone@example.com --url https://mail.example.com --tag mail
59
+ Password for this item:
60
+ added "mail" as 2
61
+
62
+ $ sefy add file ~/.ssh/id_ed25519 --tag keys
63
+ added "id_ed25519" as 3
64
+ ```
65
+
66
+ Look around.
67
+
68
+ ```
69
+ $ sefy ls
70
+ 3 id_ed25519 file [keys]
71
+ 2 mail credential [mail]
72
+ 1 bank card note [money]
73
+
74
+ $ sefy show mail
75
+ id: 2
76
+ title: mail
77
+ kind: credential
78
+ tags: mail
79
+ login: someone@example.com
80
+ password: <hidden — use sefy get>
81
+ url: https://mail.example.com
82
+ ```
83
+
84
+ Take a secret out. It goes to the clipboard and is taken back off after 45
85
+ seconds - sefy clears it only if the secret is still what is sitting there, so
86
+ anything you copied meanwhile is left alone.
87
+
88
+ ```
89
+ $ sefy get mail
90
+ copied password of "mail" to the clipboard; clearing in 45s
91
+ clipboard cleared
92
+
93
+ $ sefy get "bank card" --stdout
94
+ PIN 4815
95
+ ```
96
+
97
+ Items are addressed by title, by an exact id, or by text to search for. When
98
+ your words could mean more than one thing, sefy shows what they could mean
99
+ instead of guessing:
100
+
101
+ ```
102
+ $ sefy get ma
103
+ error: 2 items match "ma":
104
+ 4 mailing list note
105
+ 2 mail credential
106
+ narrow the text, or use an id
107
+ ```
108
+
109
+ A vault is never a trap. `sefy export` writes everything back out as plain
110
+ JSON - which is exactly as sensitive as the vault and protects nothing, so the
111
+ command makes you say so out loud:
112
+
113
+ ```
114
+ $ sefy export -o backup.json
115
+ error: export writes every secret in this vault in the clear
116
+ the resulting file protects nothing — encrypt it, or delete it when done
117
+ pass --i-know-this-writes-plaintext to go ahead
118
+ ```
119
+
120
+ Full command reference: **[lacodda.github.io/sefy](https://lacodda.github.io/sefy/)**.
121
+
122
+ ## How it works
123
+
124
+ - The master password is stretched into a key with **Argon2id**.
125
+ - The whole SQLite database is sealed with **XChaCha20-Poly1305** (AEAD).
126
+ - The file on disk is `salt ‖ nonce ‖ ciphertext`. Salt and nonce are fresh on
127
+ every save, so two saves of identical content share no prefix - and the
128
+ format version lives *inside* the ciphertext, because a version byte in the
129
+ clear would be the signature the format exists to avoid.
130
+ - The decrypted database exists **only in memory**. SQLite is never given a
131
+ path, so no page, journal or temporary file lands on disk.
132
+ - Saves are atomic: ciphertext goes to a temporary file, is synced, and is
133
+ renamed over the vault. A crash leaves either the old vault or the new one,
134
+ and never plaintext.
135
+
136
+ Details and rationale: [ADR-0001](https://github.com/lacodda/sefy/blob/main/docs/adr/0001-vault-file-format-and-cryptography.md).
137
+
138
+ ## Install
139
+
140
+ **cargo** - `cargo install sefy`
141
+
142
+ **npm** - `npm install -g sefy-cli`
143
+
144
+ **Binary releases** - grab the archive for your platform from [Releases](https://github.com/lacodda/sefy/releases/latest)
145
+ (Windows x86_64, Linux x86_64, macOS arm64), unpack and put `sefy` on your
146
+ `PATH`.
147
+
148
+ Shell completions: `sefy completions bash` (also `zsh`, `fish`, `powershell`,
149
+ `elvish`).
150
+
151
+ ## Stability
152
+
153
+ The **vault file format is stable at version 1**. Files written by this release
154
+ will stay readable: any future change to the format arrives as version 2, able
155
+ to read version 1 and migrate it. The Argon2 parameters are part of that
156
+ promise, not a tuning knob.
157
+
158
+ Released versions and what landed in each: [CHANGELOG on the Releases page](https://github.com/lacodda/sefy/releases).
159
+
160
+ ## Building
161
+
162
+ ```
163
+ cargo build --release # workspace: sefy-core (library) + sefy (CLI)
164
+ cargo test # unit, integration and doc tests
165
+ ```
166
+
167
+ Use a release build for daily work: Argon2id is deliberately expensive, and an
168
+ unoptimized build makes it several times slower still.
169
+
170
+ The library is published separately as [`sefy-core`](https://crates.io/crates/sefy-core)
171
+ if you want vaults from your own code. The documentation site lives in
172
+ [`docs/`](https://github.com/lacodda/sefy/tree/main/docs); architecture decision
173
+ records are in [`docs/adr/`](https://github.com/lacodda/sefy/tree/main/docs/adr).
174
+
175
+ ## License
176
+
177
+ [MIT](https://github.com/lacodda/sefy/blob/main/LICENSE)
package/download.js ADDED
@@ -0,0 +1,88 @@
1
+ // Downloads the sefy binary matching this package version from GitHub Releases.
2
+ // Shared by install.js (postinstall, where lifecycle scripts are allowed) and
3
+ // run.js (first-run fallback: npm v12 skips install scripts by default).
4
+ const fs = require("fs");
5
+ const https = require("https");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const pkg = require("./package.json");
10
+ const REPO = "lacodda/sefy";
11
+ // The wrapper can be patched independently of the Rust binary: an explicit
12
+ // sefy.binary field pins the release tag, otherwise it follows the version.
13
+ const TAG = (pkg.sefy && pkg.sefy.binary) || `v${pkg.version}`;
14
+
15
+ const TARGETS = {
16
+ "win32-x64": ["x86_64-pc-windows-msvc", "zip"],
17
+ "linux-x64": ["x86_64-unknown-linux-gnu", "tar.gz"],
18
+ "darwin-arm64": ["aarch64-apple-darwin", "tar.gz"],
19
+ };
20
+
21
+ const exe = process.platform === "win32" ? "sefy.exe" : "sefy";
22
+ const exePath = path.join(__dirname, exe);
23
+
24
+ function download(url, file, redirects, done) {
25
+ if (redirects > 5) return done(new Error("too many redirects"));
26
+ https
27
+ .get(url, { headers: { "user-agent": "sefy-cli" } }, (res) => {
28
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
29
+ res.resume();
30
+ return download(res.headers.location, file, redirects + 1, done);
31
+ }
32
+ if (res.statusCode !== 200) {
33
+ res.resume();
34
+ return done(new Error(`HTTP ${res.statusCode} for ${url}`));
35
+ }
36
+ const out = fs.createWriteStream(file);
37
+ res.pipe(out);
38
+ out.on("finish", () => out.close(done));
39
+ out.on("error", done);
40
+ })
41
+ .on("error", done);
42
+ }
43
+
44
+ function install(done) {
45
+ const key = `${process.platform}-${process.arch}`;
46
+ const entry = TARGETS[key];
47
+ if (!entry) {
48
+ console.error(`sefy-cli: no prebuilt binary for ${key}; install with: cargo install sefy`);
49
+ process.exit(1);
50
+ }
51
+ const [target, ext] = entry;
52
+ const name = `sefy-${TAG}-${target}`;
53
+ const url = `https://github.com/${REPO}/releases/download/${TAG}/${name}.${ext}`;
54
+ const archive = path.join(__dirname, `archive.${ext}`);
55
+
56
+ console.log(`sefy-cli: downloading ${url}`);
57
+ download(url, archive, 0, (err) => {
58
+ if (err) {
59
+ console.error(`sefy-cli: download failed: ${err.message}`);
60
+ process.exit(1);
61
+ }
62
+ // Extraction must not depend on which tar happens to be first in PATH
63
+ // (Git Bash ships a GNU tar that cannot read zip): zip goes through
64
+ // PowerShell's Expand-Archive, tar.gz through tar on Unix systems.
65
+ const result =
66
+ ext === "zip"
67
+ ? spawnSync(
68
+ "powershell.exe",
69
+ ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -LiteralPath 'archive.zip' -DestinationPath . -Force"],
70
+ { cwd: __dirname, stdio: "inherit" },
71
+ )
72
+ : spawnSync("tar", ["-xzf", `archive.${ext}`], { cwd: __dirname, stdio: "inherit" });
73
+ if (result.status !== 0) {
74
+ console.error("sefy-cli: cannot extract the archive");
75
+ process.exit(1);
76
+ }
77
+ fs.renameSync(path.join(__dirname, name, exe), exePath);
78
+ fs.rmSync(path.join(__dirname, name), { recursive: true, force: true });
79
+ fs.rmSync(archive, { force: true });
80
+ if (process.platform !== "win32") {
81
+ fs.chmodSync(exePath, 0o755);
82
+ }
83
+ console.log(`sefy-cli: installed sefy ${TAG}`);
84
+ done();
85
+ });
86
+ }
87
+
88
+ module.exports = { install, exePath };
package/install.js ADDED
@@ -0,0 +1,4 @@
1
+ // Postinstall entry: pre-downloads the binary where lifecycle scripts are
2
+ // allowed. On npm v12+ this script is skipped by default and run.js downloads
3
+ // the binary on first launch instead.
4
+ require("./download").install(() => {});
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "sefy-cli",
3
+ "version": "0.1.0",
4
+ "sefy": {
5
+ "binary": "v0.1.0"
6
+ },
7
+ "description": "An inconspicuous encrypted store for notes, credentials and files",
8
+ "license": "MIT",
9
+ "author": "Kirill Lakhtachev <lahtachev@gmail.com> (https://lacodda.com)",
10
+ "homepage": "https://lacodda.github.io/sefy/",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/lacodda/sefy.git"
14
+ },
15
+ "keywords": ["cli", "secrets", "password-manager", "encryption", "vault"],
16
+ "bin": {
17
+ "sefy": "run.js"
18
+ },
19
+ "files": ["download.js", "install.js", "run.js"],
20
+ "scripts": {
21
+ "postinstall": "node install.js"
22
+ },
23
+ "os": ["win32", "darwin", "linux"],
24
+ "engines": {
25
+ "node": ">=18"
26
+ }
27
+ }
package/run.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // Thin launcher: forwards everything to the sefy binary, downloading it on
3
+ // first run when the postinstall script was skipped (npm v12 default).
4
+ const fs = require("fs");
5
+ const { spawnSync } = require("child_process");
6
+ const { install, exePath } = require("./download");
7
+
8
+ function exec() {
9
+ const result = spawnSync(exePath, process.argv.slice(2), { stdio: "inherit" });
10
+ process.exit(result.status === null ? 1 : result.status);
11
+ }
12
+
13
+ if (fs.existsSync(exePath)) {
14
+ exec();
15
+ } else {
16
+ console.error("sefy-cli: binary not present yet - downloading it now");
17
+ install(exec);
18
+ }