Skip to content

Commit ec3906c

Browse files
feat(encrypt): main backup functionality
1 parent 2cce11c commit ec3906c

File tree

4 files changed

+327
-0
lines changed

4 files changed

+327
-0
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/key.go

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package backup
2+
3+
import (
4+
"archive/tar"
5+
"bufio"
6+
"compress/gzip"
7+
"encoding/json"
8+
"fmt"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"syscall"
13+
"time"
14+
15+
"golang.org/x/term"
16+
)
17+
18+
//structure of backed up keys
19+
type BackupData struct {
20+
21+
Timestamp time.Time `json:"timestamp"`
22+
23+
SystemInfo SystemInfo `json:"system_info"`
24+
25+
EncryptedKeys map[string]EncryptedKey `json:"encrypted_keys"`
26+
27+
Salt []byte `json:"salt"`
28+
}
29+
30+
// basic system information
31+
type SystemInfo struct {
32+
33+
Hostname string `json:"hostname"`
34+
35+
Username string `json:"username"`
36+
37+
OS string `json:"os"`
38+
}
39+
40+
//encrypted key file
41+
type EncryptedKey struct {
42+
43+
OriginalPath string `json:"original_path"`
44+
45+
KeyType string `json:"key_type"`
46+
47+
EncryptedData string `json:"encrypted_data"`
48+
49+
Permissions uint32 `json:"permissions"`
50+
}
51+
52+
//handles the backup and restore operations
53+
type BackupManager struct {
54+
config *EncryptionConfig
55+
}
56+
func NewBackupManager() *BackupManager {
57+
return &BackupManager{}
58+
}
59+
60+
// create a complete backup of keys
61+
func (bm *BackupManager) CreateBackup(customPaths []string) error {
62+
fmt.Println("Starting key backup process...")
63+
64+
//password for encryption
65+
password, err := bm.getPassword()
66+
if err != nil {
67+
return fmt.Errorf("failed to get password: %w", err)
68+
}
69+
70+
// generate salt
71+
salt, err := GenerateSalt()
72+
if err != nil {
73+
return fmt.Errorf("failed to generate salt: %w", err)
74+
}
75+
76+
bm.config = &EncryptionConfig{
77+
Password: password,
78+
Salt: salt,
79+
}
80+
81+
// search standard locations
82+
fmt.Println("searching standard key locations...")
83+
standardLocations, err := searchStandardLocations()
84+
if err != nil {
85+
return fmt.Errorf("failed to search standard locations: %w", err)
86+
}
87+
88+
//add custom paths
89+
customLocations := bm.processCustomPaths(customPaths)
90+
91+
//combine all locations
92+
allLocations := append(standardLocations, customLocations...)
93+
94+
if len(allLocations) == 0 {
95+
fmt.Println("No key locations found to backup.")
96+
return nil
97+
}
98+
99+
//create backup data
100+
backupData := &BackupData{
101+
Timestamp: time.Now(),
102+
SystemInfo: bm.getSystemInfo(),
103+
EncryptedKeys: make(map[string]EncryptedKey),
104+
Salt: salt,
105+
}
106+
107+
//encrypt and store keys
108+
fmt.Println("Encrypting keys...")
109+
for _, location := range allLocations {
110+
err := bm.processLocation(location, backupData)
111+
if err != nil {
112+
fmt.Printf("Warning: Failed to process location %s: %v\n", location.Path, err)
113+
continue
114+
}
115+
}
116+
117+
//creating tarball for the backup storing
118+
fmt.Println("Creating backup tarball...")
119+
tarballPath := fmt.Sprintf("dist/key-backup-%s.tar.gz",
120+
time.Now().Format("2006-01-02-15-04-05"))
121+
122+
err = bm.createTarball(backupData, tarballPath)
123+
if err != nil {
124+
return fmt.Errorf("failed to create tarball: %w", err)
125+
}
126+
127+
fmt.Printf("Backup completed successfully: %s\n", tarballPath)
128+
fmt.Printf("Backed up %d key files\n", len(backupData.EncryptedKeys))
129+
130+
return nil
131+
}
132+
133+
// processLocation processes a single key location
134+
func (bm *BackupManager) processLocation(location KeyLocation, backupData *BackupData) error {
135+
for _, filePath := range location.Files {
136+
//get file info for permissions
137+
fileInfo, err := os.Stat(filePath)
138+
if err != nil {
139+
140+
continue
141+
142+
}
143+
144+
// call encryption of the file
145+
encryptedData, err := EncryptFile(filePath, bm.config)
146+
if err != nil {
147+
148+
return fmt.Errorf("failed to encrypt %s: %w", filePath, err)
149+
150+
}
151+
152+
// store encrypted key
153+
keyID := filepath.Base(filePath) + "_" + strings.ReplaceAll(filePath, "/", "_")
154+
backupData.EncryptedKeys[keyID] = EncryptedKey{
155+
OriginalPath: filePath,
156+
KeyType: location.Type,
157+
EncryptedData: encryptedData,
158+
Permissions: uint32(fileInfo.Mode()),
159+
}
160+
}
161+
return nil
162+
}
163+
164+
// processCustomPaths converts custom paths to KeyLocation objects
165+
func (bm *BackupManager) processCustomPaths(customPaths []string) []KeyLocation {
166+
var locations []KeyLocation
167+
168+
for _, path := range customPaths {
169+
if path == "" {
170+
171+
continue
172+
173+
}
174+
175+
// Expand home directory
176+
if strings.HasPrefix(path, "~/") {
177+
178+
homeDir, _ := os.UserHomeDir()
179+
path = filepath.Join(homeDir, path[2:])
180+
}
181+
182+
fileInfo, err := os.Stat(path)
183+
if err != nil {
184+
fmt.Printf("Warning: Custom path %s does not exist\n", path)
185+
continue
186+
}
187+
188+
if fileInfo.IsDir() {
189+
// Either Process directory
190+
files, err := discoverKeyFiles(path)
191+
if err != nil {
192+
fmt.Printf("Warning: Failed to scan directory %s: %v\n", path, err)
193+
continue
194+
}
195+
196+
if len(files) > 0 {
197+
locations = append(locations, KeyLocation{
198+
Path: path,
199+
Type: "custom",
200+
Files: files,
201+
IsDirectory: true,
202+
})
203+
}
204+
} else {
205+
// Or Process single file
206+
locations = append(locations, KeyLocation{
207+
Path: path,
208+
Type: "custom",
209+
Files: []string{path},
210+
IsDirectory: false,
211+
})
212+
}
213+
}
214+
215+
return locations
216+
}
217+
218+
// collect basic system information
219+
func (bm *BackupManager) getSystemInfo() SystemInfo {
220+
hostname, _ := os.Hostname()
221+
username := os.Getenv("USER")
222+
if username == "" {
223+
username = os.Getenv("USERNAME")
224+
}
225+
226+
return SystemInfo{
227+
Hostname: hostname,
228+
Username: username,
229+
OS: "linux",
230+
}
231+
}
232+
233+
// prompt users for encryption password
234+
func (bm *BackupManager) getPassword() (string, error) {
235+
fmt.Print("Enter password for key encryption: ")
236+
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
237+
if err != nil {
238+
return "", err
239+
}
240+
fmt.Println()
241+
242+
password := string(bytePassword)
243+
if len(password) < 8 {
244+
return "", fmt.Errorf("password must be at least 8 characters long") ////just for better recurity - can add more such conditions
245+
}
246+
247+
return password, nil
248+
}
249+
250+
//compressed tarball with the backup data
251+
func (bm *BackupManager) createTarball(backupData *BackupData, tarballPath string) error {
252+
// if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
253+
// return err
254+
// }
255+
256+
// Create tarball file
257+
file, err := os.Create(tarballPath)
258+
if err != nil {
259+
return err
260+
}
261+
defer file.Close()
262+
263+
//gzip writer
264+
gzipWriter := gzip.NewWriter(file)
265+
defer gzipWriter.Close()
266+
267+
//tar writer
268+
tarWriter := tar.NewWriter(gzipWriter)
269+
defer tarWriter.Close()
270+
271+
// Convert backup data to JSON
272+
jsonData, err := json.MarshalIndent(backupData, "", " ")
273+
if err != nil {
274+
return err
275+
}
276+
277+
//add JSON file to tarball
278+
header := &tar.Header{
279+
Name: "backup.json",
280+
Mode: 0644,
281+
Size: int64(len(jsonData)),
282+
}
283+
284+
if err := tarWriter.WriteHeader(header); err != nil {
285+
return err
286+
}
287+
288+
if _, err := tarWriter.Write(jsonData); err != nil {
289+
return err
290+
}
291+
292+
return nil
293+
}
294+
295+
// custom key path prompt to the userss
296+
func GetCustomPaths() []string {
297+
var paths []string
298+
scanner := bufio.NewScanner(os.Stdin)
299+
300+
fmt.Println("\nEnter additional key locations (one per line, empty line to finish):")
301+
fmt.Println("Examples: ~/mykeys/, /opt/certificates/, ~/.config/app/keys")
302+
303+
for {
304+
fmt.Print("Path: ")
305+
if !scanner.Scan() {
306+
break
307+
}
308+
309+
path := strings.TrimSpace(scanner.Text())
310+
if path == "" {
311+
break
312+
}
313+
314+
paths = append(paths, path)
315+
}
316+
317+
return paths
318+
}

system/settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package system
33
const (
44
outputSysDir = "dist/sys-info"
55
outputScriptsDir = "dist"
6+
outputBackupDir = "dist/backups"
67
jsonOutputPath = outputSysDir + "/package.json"
78
scriptOutputPath = outputScriptsDir + "/setup.sh"
89
)

0 commit comments

Comments
 (0)