backdot 1.8.0 → 1.8.2

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/dist/crypto.js DELETED
@@ -1,129 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import { password as passwordPrompt, confirm } from "@inquirer/prompts";
6
- // File-format signature identifying backdot-encrypted files
7
- const MAGIC = Buffer.from("BDOT");
8
- const VERSION = 0x01;
9
- const HEADER_SIZE = MAGIC.length + 1; // 5 bytes: magic + version
10
- const SALT_SIZE = 32;
11
- const IV_SIZE = 12;
12
- const TAG_SIZE = 16;
13
- const KEY_SIZE = 32;
14
- export const KEY_FILE_PATH = path.join(os.homedir(), ".backdot.key");
15
- export const ENC_SUFFIX = ".encrypted";
16
- function deriveKey(password, salt) {
17
- return crypto.scryptSync(password, salt, KEY_SIZE, { N: 2 ** 14, r: 8, p: 1 });
18
- }
19
- export function encryptBuffer(plaintext, password) {
20
- const salt = crypto.randomBytes(SALT_SIZE);
21
- const iv = crypto.randomBytes(IV_SIZE);
22
- const key = deriveKey(password, salt);
23
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
24
- const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
25
- const tag = cipher.getAuthTag();
26
- return Buffer.concat([MAGIC, Buffer.from([VERSION]), salt, iv, tag, encrypted]);
27
- }
28
- export function decryptBuffer(data, password) {
29
- if (!isEncrypted(data)) {
30
- throw new Error("File is not encrypted (missing BDOT header).");
31
- }
32
- const minSize = HEADER_SIZE + SALT_SIZE + IV_SIZE + TAG_SIZE;
33
- if (data.length < minSize) {
34
- throw new Error("Encrypted file is too short or corrupted.");
35
- }
36
- let offset = HEADER_SIZE;
37
- const salt = data.subarray(offset, offset + SALT_SIZE);
38
- offset += SALT_SIZE;
39
- const iv = data.subarray(offset, offset + IV_SIZE);
40
- offset += IV_SIZE;
41
- const tag = data.subarray(offset, offset + TAG_SIZE);
42
- offset += TAG_SIZE;
43
- const ciphertext = data.subarray(offset);
44
- const key = deriveKey(password, salt);
45
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
46
- decipher.setAuthTag(tag);
47
- try {
48
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
49
- }
50
- catch {
51
- throw new Error("Decryption failed — wrong password or corrupted file.");
52
- }
53
- }
54
- function isEncrypted(data) {
55
- return (data.length >= HEADER_SIZE &&
56
- data[0] === MAGIC[0] &&
57
- data[1] === MAGIC[1] &&
58
- data[2] === MAGIC[2] &&
59
- data[3] === MAGIC[3] &&
60
- data[4] === VERSION);
61
- }
62
- export function checkKeyFilePermissions() {
63
- if (process.platform === "win32") {
64
- return;
65
- }
66
- if (!fs.existsSync(KEY_FILE_PATH)) {
67
- return;
68
- }
69
- const stat = fs.statSync(KEY_FILE_PATH);
70
- const mode = stat.mode & 0o777;
71
- const isAccessibleByGroupOrOthers = (mode & 0o077) !== 0;
72
- if (isAccessibleByGroupOrOthers) {
73
- throw new Error(`Key file ${KEY_FILE_PATH} has overly permissive permissions (${mode.toString(8)}).\n` +
74
- ` Run: chmod 600 ${KEY_FILE_PATH}`);
75
- }
76
- }
77
- export function saveKeyFile(password) {
78
- fs.writeFileSync(KEY_FILE_PATH, password + "\n", { mode: 0o600 });
79
- }
80
- function readKeyFile() {
81
- if (!fs.existsSync(KEY_FILE_PATH)) {
82
- return null;
83
- }
84
- checkKeyFilePermissions();
85
- return fs.readFileSync(KEY_FILE_PATH, "utf-8").trimEnd();
86
- }
87
- export async function resolvePassword() {
88
- const envPassword = process.env.BACKDOT_PASSWORD;
89
- if (envPassword) {
90
- return { password: envPassword, interactive: false };
91
- }
92
- const filePassword = readKeyFile();
93
- if (filePassword) {
94
- return { password: filePassword, interactive: false };
95
- }
96
- if (!process.stdin.isTTY) {
97
- throw new Error('Encryption is enabled but no password found.\n Run "backdot backup" interactively to create ~/.backdot.key, or set BACKDOT_PASSWORD.');
98
- }
99
- const enteredPassword = await passwordPrompt({ message: "Enter encryption password:" });
100
- if (!enteredPassword) {
101
- throw new Error("No password provided.");
102
- }
103
- return { password: enteredPassword, interactive: true };
104
- }
105
- export async function confirmPassword(password) {
106
- const confirmedPassword = await passwordPrompt({ message: "Confirm password:" });
107
- if (confirmedPassword !== password) {
108
- throw new Error("Passwords do not match.");
109
- }
110
- }
111
- export async function offerToSaveKeyFile(password) {
112
- if (!process.stdin.isTTY) {
113
- return;
114
- }
115
- if (fs.existsSync(KEY_FILE_PATH)) {
116
- return;
117
- }
118
- if (process.env.BACKDOT_PASSWORD) {
119
- return;
120
- }
121
- const save = await confirm({
122
- message: "Save password to ~/.backdot.key for automated backups?",
123
- default: true,
124
- });
125
- if (save) {
126
- saveKeyFile(password);
127
- console.log(` Created ${KEY_FILE_PATH} (permissions: 600)`);
128
- }
129
- }
@@ -1,2 +0,0 @@
1
- export declare function encrypt(plaintext: Buffer, password: string): Buffer;
2
- export declare function decrypt(encrypted: Buffer, password: string): Buffer;
@@ -1,39 +0,0 @@
1
- import crypto from "node:crypto";
2
- const ALGORITHM = "aes-256-gcm";
3
- const SALT_LENGTH = 32;
4
- const IV_LENGTH = 12;
5
- const AUTH_TAG_LENGTH = 16;
6
- const KEY_LENGTH = 32;
7
- const SCRYPT_PARAMS = { N: 2 ** 14, r: 8, p: 1 };
8
- const OVERHEAD = SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH;
9
- function deriveKey(password, salt) {
10
- return crypto.scryptSync(password, salt, KEY_LENGTH, SCRYPT_PARAMS);
11
- }
12
- export function encrypt(plaintext, password) {
13
- const salt = crypto.randomBytes(SALT_LENGTH);
14
- const iv = crypto.randomBytes(IV_LENGTH);
15
- const key = deriveKey(password, salt);
16
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
17
- const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
18
- const authTag = cipher.getAuthTag();
19
- return Buffer.concat([salt, iv, authTag, ciphertext]);
20
- }
21
- export function decrypt(encrypted, password) {
22
- if (encrypted.length < OVERHEAD) {
23
- throw new Error("Data is too short to be encrypted content.");
24
- }
25
- let offset = 0;
26
- const salt = encrypted.subarray(offset, (offset += SALT_LENGTH));
27
- const iv = encrypted.subarray(offset, (offset += IV_LENGTH));
28
- const authTag = encrypted.subarray(offset, (offset += AUTH_TAG_LENGTH));
29
- const ciphertext = encrypted.subarray(offset);
30
- const key = deriveKey(password, salt);
31
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
32
- decipher.setAuthTag(authTag);
33
- try {
34
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
35
- }
36
- catch {
37
- throw new Error("Decryption failed — wrong password or corrupted data.");
38
- }
39
- }
@@ -1 +0,0 @@
1
- export declare function errorMessage(err: unknown): string;
@@ -1,3 +0,0 @@
1
- export function errorMessage(err) {
2
- return err instanceof Error ? err.message : String(err);
3
- }
@@ -1,11 +0,0 @@
1
- export declare const KEY_FILE_PATH: string;
2
- export declare const ENC_SUFFIX = ".encrypted";
3
- export declare function checkKeyFilePermissions(): void;
4
- export declare function saveKeyFile(password: string): void;
5
- export interface PasswordResult {
6
- password: string;
7
- interactive: boolean;
8
- }
9
- export declare function resolvePassword(): Promise<PasswordResult>;
10
- export declare function confirmPassword(password: string): Promise<void>;
11
- export declare function offerToSaveKeyFile(password: string): Promise<void>;
package/dist/password.js DELETED
@@ -1,74 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { password as passwordPrompt, confirm } from "@inquirer/prompts";
5
- export const KEY_FILE_PATH = path.join(os.homedir(), ".backdot.key");
6
- export const ENC_SUFFIX = ".encrypted";
7
- export function checkKeyFilePermissions() {
8
- if (process.platform === "win32") {
9
- return;
10
- }
11
- if (!fs.existsSync(KEY_FILE_PATH)) {
12
- return;
13
- }
14
- const stat = fs.statSync(KEY_FILE_PATH);
15
- const mode = stat.mode & 0o777;
16
- const isAccessibleByGroupOrOthers = (mode & 0o077) !== 0;
17
- if (isAccessibleByGroupOrOthers) {
18
- throw new Error(`Key file ${KEY_FILE_PATH} has overly permissive permissions (${mode.toString(8)}).\n` +
19
- ` Run: chmod 600 ${KEY_FILE_PATH}`);
20
- }
21
- }
22
- export function saveKeyFile(password) {
23
- fs.writeFileSync(KEY_FILE_PATH, password + "\n", { mode: 0o600 });
24
- }
25
- function readKeyFile() {
26
- if (!fs.existsSync(KEY_FILE_PATH)) {
27
- return null;
28
- }
29
- checkKeyFilePermissions();
30
- return fs.readFileSync(KEY_FILE_PATH, "utf-8").trimEnd();
31
- }
32
- export async function resolvePassword() {
33
- const envPassword = process.env.BACKDOT_PASSWORD;
34
- if (envPassword) {
35
- return { password: envPassword, interactive: false };
36
- }
37
- const filePassword = readKeyFile();
38
- if (filePassword) {
39
- return { password: filePassword, interactive: false };
40
- }
41
- if (!process.stdin.isTTY) {
42
- throw new Error('Encryption is enabled but no password found.\n Run "backdot backup" interactively to create ~/.backdot.key, or set BACKDOT_PASSWORD.');
43
- }
44
- const enteredPassword = await passwordPrompt({ message: "Enter encryption password:" });
45
- if (!enteredPassword) {
46
- throw new Error("No password provided.");
47
- }
48
- return { password: enteredPassword, interactive: true };
49
- }
50
- export async function confirmPassword(password) {
51
- const confirmedPassword = await passwordPrompt({ message: "Confirm password:" });
52
- if (confirmedPassword !== password) {
53
- throw new Error("Passwords do not match.");
54
- }
55
- }
56
- export async function offerToSaveKeyFile(password) {
57
- if (!process.stdin.isTTY) {
58
- return;
59
- }
60
- if (fs.existsSync(KEY_FILE_PATH)) {
61
- return;
62
- }
63
- if (process.env.BACKDOT_PASSWORD) {
64
- return;
65
- }
66
- const save = await confirm({
67
- message: "Save password to ~/.backdot.key for automated backups?",
68
- default: true,
69
- });
70
- if (save) {
71
- saveKeyFile(password);
72
- console.log(` Created ${KEY_FILE_PATH} (permissions: 600)`);
73
- }
74
- }
package/dist/plist.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export declare function isScheduled(): boolean;
2
- export declare function setupLaunchd(): void;
3
- export declare function uninstallLaunchd(): void;
package/dist/plist.js DELETED
@@ -1,112 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import ora from "ora";
6
- import { logger } from "./log.js";
7
- import { errorMessage } from "./utils.js";
8
- function escapeXml(s) {
9
- return s
10
- .replace(/&/g, "&amp;")
11
- .replace(/</g, "&lt;")
12
- .replace(/>/g, "&gt;")
13
- .replace(/"/g, "&quot;");
14
- }
15
- const LABEL = "com.backdot.daemon";
16
- const PLIST_PATH = path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
17
- function getScriptPath() {
18
- const currentDir = path.dirname(new URL(import.meta.url).pathname);
19
- return path.resolve(currentDir, "cli.js");
20
- }
21
- function buildPlist() {
22
- const nodePath = process.execPath;
23
- const scriptPath = getScriptPath();
24
- const workingDir = path.dirname(scriptPath);
25
- const logPath = path.join(os.homedir(), ".backdot", "launchd.log");
26
- return `<?xml version="1.0" encoding="UTF-8"?>
27
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28
- <plist version="1.0">
29
- <dict>
30
- <key>Label</key>
31
- <string>${LABEL}</string>
32
- <key>ProgramArguments</key>
33
- <array>
34
- <string>${escapeXml(nodePath)}</string>
35
- <string>${escapeXml(scriptPath)}</string>
36
- <string>--backup</string>
37
- </array>
38
- <key>WorkingDirectory</key>
39
- <string>${escapeXml(workingDir)}</string>
40
- <key>StartCalendarInterval</key>
41
- <dict>
42
- <key>Hour</key>
43
- <integer>2</integer>
44
- <key>Minute</key>
45
- <integer>0</integer>
46
- </dict>
47
- <key>StandardOutPath</key>
48
- <string>${escapeXml(logPath)}</string>
49
- <key>StandardErrorPath</key>
50
- <string>${escapeXml(logPath)}</string>
51
- <key>RunAtLoad</key>
52
- <false/>
53
- </dict>
54
- </plist>`;
55
- }
56
- export function isScheduled() {
57
- if (!fs.existsSync(PLIST_PATH)) {
58
- return false;
59
- }
60
- try {
61
- const output = execFileSync("launchctl", ["list", LABEL], { encoding: "utf-8", stdio: "pipe" });
62
- return output.includes(LABEL);
63
- }
64
- catch {
65
- return false;
66
- }
67
- }
68
- export function setupLaunchd() {
69
- const spinner = ora("Installing schedule").start();
70
- const plistContent = buildPlist();
71
- const dir = path.dirname(PLIST_PATH);
72
- if (!fs.existsSync(dir)) {
73
- fs.mkdirSync(dir, { recursive: true });
74
- }
75
- try {
76
- execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
77
- }
78
- catch {
79
- // Not loaded, that's fine
80
- }
81
- fs.writeFileSync(PLIST_PATH, plistContent);
82
- logger.info(`Plist written to ${PLIST_PATH}`);
83
- try {
84
- execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
85
- spinner.succeed("Daily backup scheduled (02:00)");
86
- console.log();
87
- logger.info("Launchd job loaded");
88
- }
89
- catch (err) {
90
- const msg = errorMessage(err);
91
- spinner.fail(`Failed to load launchd job: ${msg}`);
92
- console.log();
93
- logger.error(`Failed to load launchd job: ${msg}`);
94
- throw new Error(`Failed to load launchd job: ${msg}`, { cause: err });
95
- }
96
- }
97
- export function uninstallLaunchd() {
98
- const spinner = ora("Removing schedule").start();
99
- try {
100
- execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
101
- logger.info("Launchd job unloaded");
102
- }
103
- catch {
104
- logger.info("Launchd job was not loaded");
105
- }
106
- if (fs.existsSync(PLIST_PATH)) {
107
- fs.unlinkSync(PLIST_PATH);
108
- logger.info(`Plist removed: ${PLIST_PATH}`);
109
- }
110
- spinner.succeed("Schedule removed");
111
- console.log();
112
- }
package/dist/resolve.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { Config } from "./config.js";
2
- /**
3
- * Resolve all file entries to absolute paths.
4
- * Skips entries that fail resolution and logs warnings.
5
- */
6
- export declare function resolveFiles(config: Config): string[];
package/dist/resolve.js DELETED
@@ -1,44 +0,0 @@
1
- import fs from "node:fs";
2
- import fg from "fast-glob";
3
- import { logger } from "./log.js";
4
- import { errorMessage, uniq } from "./utils.js";
5
- function resolveGlobs(patterns) {
6
- if (patterns.length === 0) {
7
- return [];
8
- }
9
- try {
10
- return fg.sync(patterns, { absolute: true, dot: true, onlyFiles: true });
11
- }
12
- catch (err) {
13
- logger.warn(`Glob pattern resolution failed: ${errorMessage(err)}`);
14
- return [];
15
- }
16
- }
17
- const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
18
- /**
19
- * Resolve all file entries to absolute paths.
20
- * Skips entries that fail resolution and logs warnings.
21
- */
22
- export function resolveFiles(config) {
23
- const unique = uniq(resolveGlobs(config.paths));
24
- return unique.filter((filePath) => {
25
- try {
26
- fs.accessSync(filePath, fs.constants.R_OK);
27
- const stat = fs.statSync(filePath);
28
- if (!stat.isFile()) {
29
- logger.warn(`Not a regular file, skipping: ${filePath}`);
30
- return false;
31
- }
32
- if (stat.size > LARGE_FILE_THRESHOLD) {
33
- const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
34
- logger.warn(`Large file (${sizeMB} MB), skipping: ${filePath}`);
35
- return false;
36
- }
37
- return true;
38
- }
39
- catch {
40
- logger.warn(`File not readable, skipping: ${filePath}`);
41
- return false;
42
- }
43
- });
44
- }