Merge pull request 'merge: planned additions' (#5) from dev/finalized-features into main
Reviewed-on: #5
This commit is contained in:
commit
44b2874780
12 changed files with 1427 additions and 189 deletions
|
|
@ -9,3 +9,4 @@ xml-template
|
|||
cache
|
||||
etc
|
||||
library
|
||||
watch
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@
|
|||
cache
|
||||
library
|
||||
etc
|
||||
watch
|
||||
|
|
|
|||
5
Makefile
Normal file
5
Makefile
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
build:
|
||||
go build -o bin/main app/gopherbook/main.go
|
||||
|
||||
clean:
|
||||
rm -rf watch etc library cache
|
||||
55
README.md
55
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# Gopherbook – Self-Hosted Comic Library & CBZ Reader
|
||||
# Gopherbook – Self-Hosted Comic Library & CBZ/CBT Reader
|
||||
|
||||
Gopherbook is a lightweight, single-binary, self-hosted web comic reader and library manager written in Go.
|
||||
It is designed for people who want full control over their digital comic collection (CBZ files), including support for password-protected/encrypted archives, per-user libraries, tagging, automatic organization, and a clean modern reader.
|
||||
It is designed for people who want full control over their digital comic collection (CBZ/CBT files), including support for password-protected/encrypted archives, per-user libraries, tagging, automatic organization, and a clean modern reader.
|
||||
|
||||
## License
|
||||
|
||||
|
|
@ -9,10 +9,11 @@ It is designed for people who want full control over their digital comic collect
|
|||
|
||||
## Features
|
||||
|
||||
- Upload & read `.cbz` (ZIP-based) comics directly in the browser
|
||||
- Upload & read `.cbz` (ZIP-based) or `.cbt` (TAR-based) comics directly in the browser
|
||||
- **Watch folder support for bulk imports** – drop CBZ/CBT files into your watch folder and they're automatically imported
|
||||
- Supports 8 Megapixel images at 512MB memory limits
|
||||
- (increase in bash script for higher Megapixels or remove the limitation if you don't care)
|
||||
- Full support for password-protected/encrypted CBZ files (AES-256 via yeka/zip)
|
||||
- Full support for password-protected/encrypted CBZ files (AES-256 via yeka/zip) or CBT files (AES-256-CFB Openssl)
|
||||
- Automatically tries all previously successful passwords when opening a new encrypted comic
|
||||
- Persists discovered passwords securely (AES-encrypted on disk, key derived from your login password)
|
||||
- Extracts ComicInfo.xml metadata (title, series, number, writer, inker, tags, story arc, etc.)
|
||||
|
|
@ -42,7 +43,6 @@ It is designed for people who want full control over their digital comic collect
|
|||
- Or just download a pre-built binary from Releases (when available)
|
||||
|
||||
### Quick start (from source)
|
||||
|
||||
```bash
|
||||
git clone https://codeberg.org/riomoo/gopherbook.git
|
||||
cd gopherbook
|
||||
|
|
@ -53,11 +53,10 @@ go build -o gopherbook app/gopherbook/main.go
|
|||
Then open http://localhost:8080 in your browser.
|
||||
|
||||
## If you want to use this with podman:
|
||||
|
||||
```bash
|
||||
git clone https://codeberg.org/riomoo/gopherbook.git
|
||||
cd gopherbook
|
||||
./bash-scripts/run.sh
|
||||
./scripts-bash/run.sh
|
||||
```
|
||||
|
||||
Then open http://localhost:12010 in your browser.
|
||||
|
|
@ -65,23 +64,56 @@ Then open http://localhost:12010 in your browser.
|
|||
### First launch
|
||||
1. On first run there are no users → registration is open
|
||||
2. Create the first account → this user automatically becomes admin
|
||||
3. Log in → start uploading CBZ files
|
||||
3. Log in → start uploading CBZ/CBT files
|
||||
|
||||
## Directory layout after first login
|
||||
|
||||
```
|
||||
./library/username/ ← your comics (organized or Unorganized/)
|
||||
./library/username/comics.json ← metadata index
|
||||
./library/username/tags.json ← tag definitions & counts
|
||||
./library/username/passwords.json ← encrypted password vault (AES)
|
||||
./cache/covers/username/ ← generated cover thumbnails
|
||||
./watch/username/ ← watch folder for bulk imports (auto-scanned)
|
||||
./etc/users.json ← user accounts (bcrypt hashes)
|
||||
./etc/admin.json ← admin settings (registration toggle)
|
||||
```
|
||||
|
||||
## Watch folder for bulk imports
|
||||
|
||||
Gopherbook includes an automatic watch folder system that makes bulk importing comics effortless:
|
||||
|
||||
- **Per-user watch folders**: Each user gets their own watch folder at `./watch/[username]/`
|
||||
- **Automatic scanning**: The system checks for new CBZ/CBT files every 10 seconds
|
||||
- **Smart debouncing**: Waits 5 seconds after detecting files to ensure they're fully copied
|
||||
- **File validation**: Checks that files aren't still being written before importing
|
||||
- **Duplicate handling**: Automatically renames files if they already exist (adds _1, _2, etc.)
|
||||
- **Zero configuration**: Just drop CBZ/CBT files into your watch folder and they appear in your library
|
||||
|
||||
### How to use
|
||||
|
||||
1. After logging in, your personal watch folder is at `./watch/[yourusername]/`
|
||||
2. Copy or move CBZ/CBT files into this folder using any method:
|
||||
- Direct file copy/paste
|
||||
- SCP/SFTP upload
|
||||
- Network share mount
|
||||
- Automated scripts
|
||||
3. Within ~15 seconds, files are automatically imported to your library
|
||||
4. Files are **moved** (not copied) to preserve disk space
|
||||
5. Check the API endpoint `/api/watch-folder` to see pending files
|
||||
|
||||
**Example workflow:**
|
||||
```bash
|
||||
# Bulk copy comics to your watch folder
|
||||
cp ~/Downloads/*.cbz ./watch/myusername/
|
||||
cp ~/Downloads/*.cbt ./watch/myusername/
|
||||
|
||||
# Wait ~15 seconds, then check your library in the web UI
|
||||
# All comics will be imported and organized automatically
|
||||
```
|
||||
|
||||
## How encrypted/password-protected comics work
|
||||
|
||||
- When you upload or scan an encrypted CBZ that has no known password yet, the server marks it as Encrypted = true.
|
||||
- When you upload or scan an encrypted CBZ/CBT that has no known password yet, the server marks it as Encrypted = true.
|
||||
- The first time you open it in the reader, a password prompt appears.
|
||||
- If the password is correct, Gopherbook:
|
||||
- Stores the password (encrypted with a key derived from your login password)
|
||||
|
|
@ -152,7 +184,7 @@ Please open an issue first for bigger changes.
|
|||
- Everyone who hoards comics ❤️
|
||||
|
||||
Enjoy your library!
|
||||
— Happy reading with Gopherbook
|
||||
– Happy reading with Gopherbook
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
|
@ -160,6 +192,7 @@ Enjoy your library!
|
|||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -769,14 +769,14 @@
|
|||
<div id="mainSection" class="hidden">
|
||||
<div id="message"></div>
|
||||
|
||||
<div class="upload-section">
|
||||
<h2 style="margin-bottom: 16px; color: #c9d1d9;">Upload Comic</h2>
|
||||
<div class="form-group">
|
||||
<label>Select File (CBZ)</label>
|
||||
<input type="file" id="fileUpload" accept=".cbz">
|
||||
</div>
|
||||
<button onclick="uploadComic()">Upload</button>
|
||||
</div>
|
||||
<div class="upload-section">
|
||||
<h2 style="margin-bottom: 16px; color: #c9d1d9;">Upload Comic</h2>
|
||||
<div class="form-group">
|
||||
<label>Select File (CBZ or CBT)</label>
|
||||
<input type="file" id="fileUpload" accept=".cbz,.cbt">
|
||||
</div>
|
||||
<button onclick="uploadComic()">Upload</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<h3 style="margin-bottom: 12px; color: #c9d1d9; font-size: 16px;">Filter by Tags</h3>
|
||||
|
|
|
|||
204
scripts-bash/cbt.sh
Executable file
204
scripts-bash/cbt.sh
Executable file
|
|
@ -0,0 +1,204 @@
|
|||
#!/bin/bash
|
||||
|
||||
# CBT (Comic Book Tar) Creator and Extractor
|
||||
# Creates and extracts .cbt files with optional AES encryption
|
||||
# Compatible with Go server encryption format
|
||||
|
||||
set -e
|
||||
|
||||
show_usage() {
|
||||
echo "Usage:"
|
||||
echo " Create: $0 create -i <input_directory> -o <output_file> [-p <password>]"
|
||||
echo " Extract: $0 extract -i <input_file> -o <output_directory> [-p <password>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i Input directory/file"
|
||||
echo " -o Output file/directory"
|
||||
echo " -p Password for encryption/decryption (optional)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 create -i ./comics -o mycomic.cbt"
|
||||
echo " $0 create -i ./comics -o mycomic.cbt -p mysecretpass"
|
||||
echo " $0 extract -i mycomic.cbt -o ./extracted"
|
||||
echo " $0 extract -i mycomic.cbt -o ./extracted -p mysecretpass"
|
||||
echo ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
MODE="$1"
|
||||
shift || show_usage
|
||||
|
||||
INPUT=""
|
||||
OUTPUT=""
|
||||
PASSWORD=""
|
||||
|
||||
while getopts "i:o:p:h" opt; do
|
||||
case $opt in
|
||||
i) INPUT="$OPTARG" ;;
|
||||
o) OUTPUT="$OPTARG" ;;
|
||||
p) PASSWORD="$OPTARG" ;;
|
||||
h) show_usage ;;
|
||||
*) show_usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate mode
|
||||
if [ "$MODE" != "create" ] && [ "$MODE" != "extract" ]; then
|
||||
echo "Error: First argument must be 'create' or 'extract'"
|
||||
show_usage
|
||||
fi
|
||||
|
||||
# Validate required arguments
|
||||
if [ -z "$INPUT" ] || [ -z "$OUTPUT" ]; then
|
||||
echo "Error: Input and output are required"
|
||||
show_usage
|
||||
fi
|
||||
|
||||
# Encrypt data using AES-CFB
|
||||
encrypt_data() {
|
||||
local input_file="$1"
|
||||
local output_file="$2"
|
||||
local password="$3"
|
||||
|
||||
# Derive key using SHA256 (same as Go)
|
||||
local key=$(echo -n "$password" | sha256sum | cut -d' ' -f1 | xxd -r -p | base64)
|
||||
|
||||
# Generate random IV (16 bytes for AES)
|
||||
local iv=$(openssl rand -base64 16)
|
||||
|
||||
# Encrypt using AES-256-CFB
|
||||
# First write IV, then encrypted data
|
||||
echo -n "$iv" | base64 -d > "$output_file"
|
||||
openssl enc -aes-256-cfb -K "$(echo -n "$password" | sha256sum | cut -d' ' -f1)" -iv "$(echo -n "$iv" | base64 -d | xxd -p -c 256)" -in "$input_file" >> "$output_file"
|
||||
}
|
||||
|
||||
# Decrypt data using AES-CFB (compatible with Go implementation)
|
||||
decrypt_data() {
|
||||
local input_file="$1"
|
||||
local output_file="$2"
|
||||
local password="$3"
|
||||
|
||||
# Extract IV (first 16 bytes)
|
||||
local iv_hex=$(head -c 16 "$input_file" | xxd -p -c 256)
|
||||
|
||||
# Extract ciphertext (rest of file)
|
||||
tail -c +17 "$input_file" > "${output_file}.tmp"
|
||||
|
||||
# Decrypt using AES-256-CFB
|
||||
if ! openssl enc -d -aes-256-cfb -K "$(echo -n "$password" | sha256sum | cut -d' ' -f1)" -iv "$iv_hex" -in "${output_file}.tmp" -out "$output_file" 2>/dev/null; then
|
||||
rm -f "${output_file}.tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "${output_file}.tmp"
|
||||
return 0
|
||||
}
|
||||
|
||||
# CREATE MODE
|
||||
create_cbt() {
|
||||
INPUT_DIR="$INPUT"
|
||||
OUTPUT_FILE="$OUTPUT"
|
||||
|
||||
# Check if input directory exists
|
||||
if [ ! -d "$INPUT_DIR" ]; then
|
||||
echo "Error: Input directory '$INPUT_DIR' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure output has .cbt extension
|
||||
if [[ ! "$OUTPUT_FILE" =~ \.cbt$ ]]; then
|
||||
OUTPUT_FILE="${OUTPUT_FILE}.cbt"
|
||||
fi
|
||||
|
||||
# Create unencrypted tar archive
|
||||
echo "Creating tar archive from $INPUT_DIR..."
|
||||
TMP_TAR=$(mktemp)
|
||||
|
||||
# Create tar file, preserving relative paths
|
||||
tar -cf "$TMP_TAR" -C "$INPUT_DIR" .
|
||||
|
||||
# List files that were added
|
||||
echo ""
|
||||
echo "Files added:"
|
||||
tar -tf "$TMP_TAR"
|
||||
echo ""
|
||||
|
||||
if [ -n "$PASSWORD" ]; then
|
||||
# Encrypted mode
|
||||
echo "Encrypting with AES-CFB (Go-compatible format)..."
|
||||
|
||||
encrypt_data "$TMP_TAR" "$OUTPUT_FILE" "$PASSWORD"
|
||||
|
||||
rm "$TMP_TAR"
|
||||
echo "Created encrypted CBT: $OUTPUT_FILE"
|
||||
else
|
||||
# Unencrypted mode - just move the tar file
|
||||
mv "$TMP_TAR" "$OUTPUT_FILE"
|
||||
echo "Created unencrypted CBT: $OUTPUT_FILE"
|
||||
fi
|
||||
|
||||
echo "Done!"
|
||||
}
|
||||
|
||||
# EXTRACT MODE
|
||||
extract_cbt() {
|
||||
INPUT_FILE="$INPUT"
|
||||
OUTPUT_DIR="$OUTPUT"
|
||||
|
||||
# Check if input file exists
|
||||
if [ ! -f "$INPUT_FILE" ]; then
|
||||
echo "Error: Input file '$INPUT_FILE' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
TMP_TAR=$(mktemp)
|
||||
|
||||
# Check if file is encrypted by trying to read as tar first
|
||||
if tar -tf "$INPUT_FILE" >/dev/null 2>&1; then
|
||||
# File is unencrypted tar
|
||||
cp "$INPUT_FILE" "$TMP_TAR"
|
||||
else
|
||||
# File appears to be encrypted
|
||||
if [ -z "$PASSWORD" ]; then
|
||||
echo "Error: File appears to be encrypted. Please provide password with -p flag."
|
||||
rm "$TMP_TAR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Decrypting $INPUT_FILE..."
|
||||
|
||||
if ! decrypt_data "$INPUT_FILE" "$TMP_TAR" "$PASSWORD"; then
|
||||
echo "Error: Failed to decrypt. Wrong password or corrupted file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Decryption successful!"
|
||||
fi
|
||||
|
||||
# Extract tar archive
|
||||
echo "Extracting to $OUTPUT_DIR..."
|
||||
|
||||
if tar -xf "$TMP_TAR" -C "$OUTPUT_DIR" 2>/dev/null; then
|
||||
echo ""
|
||||
echo "Files extracted:"
|
||||
tar -tf "$TMP_TAR"
|
||||
echo ""
|
||||
echo "Extraction complete!"
|
||||
else
|
||||
rm "$TMP_TAR"
|
||||
echo "Error: Failed to extract. File may be corrupted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "$TMP_TAR"
|
||||
}
|
||||
|
||||
if [ "$MODE" = "create" ]; then
|
||||
create_cbt
|
||||
elif [ "$MODE" = "extract" ]; then
|
||||
extract_cbt
|
||||
fi
|
||||
|
|
@ -12,7 +12,7 @@ if [ $? -ne 0 ]; then
|
|||
fi
|
||||
|
||||
# Ensure directories exist with correct permissions
|
||||
mkdir -p ./library ./cache ./etc
|
||||
mkdir -p ./library ./cache ./etc ./watch
|
||||
|
||||
if podman container exists "$CONTAINER_NAME"; then
|
||||
echo "Container '$CONTAINER_NAME' already exists. Stopping and removing it..."
|
||||
|
|
@ -29,6 +29,7 @@ podman run -d --name "$CONTAINER_NAME" \
|
|||
-v ./library:/app/library \
|
||||
-v ./cache:/app/cache \
|
||||
-v ./etc:/app/etc \
|
||||
-v ./watch:/app/watch \
|
||||
"$IMAGE_NAME"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
3
scripts-bash/test-cbt.sh
Executable file
3
scripts-bash/test-cbt.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
user="$(gpg2 -qd /home/moo/.local/pdf-keys/userkey)"
|
||||
mkdir -p out
|
||||
./cbt.sh create -i $1 -o ./out/$1.cbt -p $user
|
||||
231
scripts-go/cbt.go
Normal file
231
scripts-go/cbt.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
inputDir := flag.String("input", "", "Input directory containing comic files")
|
||||
outputFile := flag.String("output", "", "Output .cbt file")
|
||||
password := flag.String("password", "", "Password for encryption (leave empty for no encryption)")
|
||||
flag.Parse()
|
||||
|
||||
if *inputDir == "" || *outputFile == "" {
|
||||
fmt.Println("Usage: cbt-creator -input <directory> -output <file.cbt> [-password <password>]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Ensure output has .cbt extension
|
||||
if !strings.HasSuffix(strings.ToLower(*outputFile), ".cbt") {
|
||||
*outputFile += ".cbt"
|
||||
}
|
||||
|
||||
var err error
|
||||
if *password != "" {
|
||||
err = createEncryptedCBT(*inputDir, *outputFile, *password)
|
||||
fmt.Printf("Created encrypted CBT: %s\n", *outputFile)
|
||||
} else {
|
||||
err = createUnencryptedCBT(*inputDir, *outputFile)
|
||||
fmt.Printf("Created unencrypted CBT: %s\n", *outputFile)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func createUnencryptedCBT(inputDir, outputPath string) error {
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
tw := tar.NewWriter(outFile)
|
||||
defer tw.Close()
|
||||
|
||||
// Collect all files
|
||||
var files []string
|
||||
err = filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add files to tar
|
||||
for _, file := range files {
|
||||
relPath, err := filepath.Rel(inputDir, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read file
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write tar header
|
||||
hdr := &tar.Header{
|
||||
Name: relPath,
|
||||
Mode: 0600,
|
||||
Size: int64(len(data)),
|
||||
}
|
||||
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write file data
|
||||
if _, err := tw.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Added: %s\n", relPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createEncryptedCBT(inputDir, outputPath, password string) error {
|
||||
// Create temporary unencrypted tar
|
||||
tmpTar := outputPath + ".tmp"
|
||||
tmpFile, err := os.Create(tmpTar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tw := tar.NewWriter(tmpFile)
|
||||
|
||||
// Collect all files
|
||||
var files []string
|
||||
err = filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpTar)
|
||||
return err
|
||||
}
|
||||
|
||||
// Add files to tar
|
||||
for _, file := range files {
|
||||
relPath, err := filepath.Rel(inputDir, file)
|
||||
if err != nil {
|
||||
tw.Close()
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpTar)
|
||||
return err
|
||||
}
|
||||
|
||||
// Read file
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
tw.Close()
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpTar)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write tar header
|
||||
hdr := &tar.Header{
|
||||
Name: relPath,
|
||||
Mode: 0600,
|
||||
Size: int64(len(data)),
|
||||
}
|
||||
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
tw.Close()
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpTar)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write file data
|
||||
if _, err := tw.Write(data); err != nil {
|
||||
tw.Close()
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpTar)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Added: %s\n", relPath)
|
||||
}
|
||||
|
||||
tw.Close()
|
||||
tmpFile.Close()
|
||||
|
||||
// Read the tar file
|
||||
tarData, err := os.ReadFile(tmpTar)
|
||||
if err != nil {
|
||||
os.Remove(tmpTar)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Encrypting...")
|
||||
|
||||
// Encrypt
|
||||
key := deriveKey(password)
|
||||
encrypted, err := encryptAES(tarData, key)
|
||||
if err != nil {
|
||||
os.Remove(tmpTar)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write encrypted file
|
||||
err = os.WriteFile(outputPath, encrypted, 0644)
|
||||
os.Remove(tmpTar)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func deriveKey(password string) []byte {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func encryptAES(plaintext []byte, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate IV
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := rand.Read(iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
stream.XORKeyStream(ciphertext, plaintext)
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
return append(iv, ciphertext...), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue