Skip to content

Commit 388fe81

Browse files
authored
Merge pull request #2 from mdgspace/ronin/encryp-backup
SSH/GPG encryption and compressing to tarbell
2 parents edfcd74 + 7eb6592 commit 388fe81

File tree

9 files changed

+580
-35
lines changed

9 files changed

+580
-35
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
module github.com/mdgspace/sysreplicate
22

33
go 1.24.3
4+
5+
require golang.org/x/term v0.32.0
6+
7+
require golang.org/x/sys v0.33.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
2+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
3+
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
4+
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=

system/backup/encrypt.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package backup
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"encoding/base64"
8+
"fmt"
9+
"io"
10+
"os"
11+
)
12+
13+
//simplified encryption config without password
14+
type EncryptionConfig struct {
15+
Key []byte // Direct 32-byte key instead of password+salt
16+
}
17+
18+
//AES-GCM encryption with direct key (no password derivation)
19+
func EncryptFile(filePath string, config *EncryptionConfig) (string, error) {
20+
data, err := os.ReadFile(filePath)
21+
if err != nil {
22+
return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
23+
}
24+
25+
//use direct key (no password derivation)
26+
block, err := aes.NewCipher(config.Key)
27+
if err != nil {
28+
return "", fmt.Errorf("failed to create cipher: %w", err)
29+
}
30+
31+
// GCM mode from AES block
32+
gcm, err := cipher.NewGCM(block)
33+
if err != nil {
34+
return "", fmt.Errorf("failed to create GCM: %w", err)
35+
}
36+
37+
// Generate nonce
38+
nonce := make([]byte, gcm.NonceSize())
39+
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
40+
return "", fmt.Errorf("failed to generate nonce: %w", err)
41+
}
42+
43+
//encrypt data with nonce
44+
ciphertext := gcm.Seal(nonce, nonce, data, nil)
45+
46+
//encode to base64
47+
encoded := base64.StdEncoding.EncodeToString(ciphertext)
48+
return encoded, nil
49+
}
50+
51+
//generate a random 32-byte key for AES-256
52+
func GenerateKey() ([]byte, error) {
53+
key := make([]byte, 32) // 32 bytes for AES-256
54+
_, err := rand.Read(key)
55+
return key, err
56+
}

system/backup/key.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package backup
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"time"
10+
"github.com/mdgspace/sysreplicate/system/output"
11+
)
12+
13+
//backup and restore operations
14+
type BackupManager struct {
15+
config *EncryptionConfig
16+
}
17+
18+
func NewBackupManager() *BackupManager {
19+
return &BackupManager{}
20+
}
21+
22+
//create a complete backup of keys (no password required)
23+
func (bm *BackupManager) CreateBackup(customPaths []string) error {
24+
fmt.Println("Starting key backup process...")
25+
26+
//generate random encryption key (no password needed)
27+
key, err := GenerateKey()
28+
if err != nil {
29+
return fmt.Errorf("failed to generate encryption key: %w", err)
30+
}
31+
32+
bm.config = &EncryptionConfig{
33+
Key: key,
34+
}
35+
36+
// search standard locations
37+
fmt.Println("searching standard key locations...")
38+
standardLocations, err := searchStandardLocations()
39+
if err != nil {
40+
return fmt.Errorf("failed to search standard locations: %w", err)
41+
}
42+
43+
//add custom paths
44+
customLocations := bm.processCustomPaths(customPaths)
45+
46+
//combine all locations
47+
allLocations := append(standardLocations, customLocations...)
48+
if len(allLocations) == 0 {
49+
fmt.Println("No key locations found to backup.")
50+
return nil
51+
}
52+
53+
//create backup data
54+
backupData := &output.BackupData{
55+
Timestamp: time.Now(),
56+
SystemInfo: bm.getSystemInfo(),
57+
EncryptedKeys: make(map[string]output.EncryptedKey),
58+
EncryptionKey: key, // Store the key in backup data
59+
}
60+
61+
//encrypt and store keys
62+
fmt.Println("Encrypting keys...")
63+
for _, location := range allLocations {
64+
err := bm.processLocation(location, backupData)
65+
if err != nil {
66+
fmt.Printf("Warning: Failed to process location %s: %v\n", location.Path, err)
67+
continue
68+
}
69+
}
70+
71+
//creating tarball for the backup storing
72+
fmt.Println("Creating backup tarball...")
73+
tarballPath := fmt.Sprintf("dist/key-backup-%s.tar.gz",
74+
time.Now().Format("2006-01-02-15-04-05"))
75+
err = output.CreateBackupTarball(backupData, tarballPath)
76+
if err != nil {
77+
return fmt.Errorf("failed to create tarball: %w", err)
78+
}
79+
80+
fmt.Printf("Backup completed successfully: %s\n", tarballPath)
81+
fmt.Printf("Backed up %d key files\n", len(backupData.EncryptedKeys))
82+
return nil
83+
}
84+
85+
86+
// processLocation processes a single key location
87+
func (bm *BackupManager) processLocation(location KeyLocation, backupData *output.BackupData) error {
88+
for _, filePath := range location.Files {
89+
//get file info for permissions
90+
fileInfo, err := os.Stat(filePath)
91+
if err != nil {
92+
continue
93+
}
94+
95+
// call encryption of the file
96+
encryptedData, err := EncryptFile(filePath, bm.config)
97+
if err != nil {
98+
return fmt.Errorf("failed to encrypt %s: %w", filePath, err)
99+
}
100+
101+
// store encrypted key
102+
keyID := filepath.Base(filePath) + "_" + strings.ReplaceAll(filePath, "/", "_")
103+
backupData.EncryptedKeys[keyID] = output.EncryptedKey{
104+
OriginalPath: filePath,
105+
KeyType: location.Type,
106+
EncryptedData: encryptedData,
107+
Permissions: uint32(fileInfo.Mode()),
108+
}
109+
}
110+
return nil
111+
}
112+
113+
// processCustomPaths converts custom paths to KeyLocation objects
114+
func (bm *BackupManager) processCustomPaths(customPaths []string) []KeyLocation {
115+
var locations []KeyLocation
116+
for _, path := range customPaths {
117+
if path == "" {
118+
continue
119+
}
120+
121+
// Expand home directory
122+
if strings.HasPrefix(path, "~/") {
123+
124+
homeDir, _ := os.UserHomeDir()
125+
path = filepath.Join(homeDir, path[2:])
126+
}
127+
128+
fileInfo, err := os.Stat(path)
129+
if err != nil {
130+
fmt.Printf("Warning: Custom path %s does not exist\n", path)
131+
continue
132+
}
133+
134+
if fileInfo.IsDir() {
135+
// Either Process directory
136+
files, err := discoverKeyFiles(path)
137+
if err != nil {
138+
fmt.Printf("Warning: Failed to scan directory %s: %v\n", path, err)
139+
continue
140+
}
141+
142+
if len(files) > 0 {
143+
locations = append(locations, KeyLocation{
144+
Path: path,
145+
Type: "custom",
146+
Files: files,
147+
IsDirectory: true,
148+
})
149+
}
150+
} else {
151+
// Or Process single file
152+
locations = append(locations, KeyLocation{
153+
Path: path,
154+
Type: "custom",
155+
Files: []string{path},
156+
IsDirectory: false,
157+
})
158+
}
159+
}
160+
return locations
161+
}
162+
163+
// collect basic system information
164+
func (bm *BackupManager) getSystemInfo() output.SystemInfo {
165+
hostname, _ := os.Hostname()
166+
username := os.Getenv("USER")
167+
if username == "" {
168+
username = os.Getenv("USERNAME")
169+
}
170+
return output.SystemInfo{
171+
Hostname: hostname,
172+
Username: username,
173+
OS: "linux",
174+
}
175+
}
176+
177+
// custom key path prompt to the userss
178+
func GetCustomPaths() []string {
179+
var paths []string
180+
scanner := bufio.NewScanner(os.Stdin)
181+
fmt.Println("\nEnter additional key locations (one per line, empty line to finish):")
182+
fmt.Println("Examples: ~/mykeys/, /opt/certificates/, ~/.config/app/keys")
183+
fmt.Println("Note: .ssh and .gnupg are default scouting locations")
184+
185+
for {
186+
fmt.Print("Path: ")
187+
if !scanner.Scan() {
188+
break
189+
}
190+
191+
path := strings.TrimSpace(scanner.Text())
192+
if path == "" {
193+
break
194+
}
195+
paths = append(paths, path)
196+
}
197+
return paths
198+
}

0 commit comments

Comments
 (0)