1package commands
2
3import (
4	"crypto/rand"
5	"encoding/json"
6	"errors"
7	"flag"
8	"fmt"
9	"io"
10	"os"
11	"path/filepath"
12	"strconv"
13	"strings"
14	"time"
15
16	"github.com/jesseduffield/horcrux/pkg/multiplexing"
17	"github.com/jesseduffield/horcrux/pkg/shamir"
18)
19
20func SplitWithPrompt(path string) error {
21	total, threshold, err := obtainTotalAndThreshold()
22	if err != nil {
23		return err
24	}
25
26	return Split(path, path, total, threshold)
27}
28
29func Split(path string, destination string, total int, threshold int) error {
30	key, err := generateKey()
31	if err != nil {
32		return err
33	}
34
35	keyFragments, err := shamir.Split(key, total, threshold)
36	if err != nil {
37		return err
38	}
39
40	timestamp := time.Now().Unix()
41
42	file, err := os.Open(path)
43	if err != nil {
44		return err
45	}
46	originalFilename := filepath.Base(path)
47
48	// create destination directory if it does not already exist.
49	stat, err := os.Stat(destination)
50	if err != nil {
51		if !os.IsNotExist(err) {
52			return err
53		}
54		if err := os.MkdirAll(destination, os.ModePerm); err != nil {
55			return err
56		}
57	} else {
58		if !stat.IsDir() {
59			return errors.New("Destination must be a directory")
60		}
61	}
62
63	horcruxFiles := make([]*os.File, total)
64	for i := range horcruxFiles {
65		index := i + 1
66
67		headerBytes, err := json.Marshal(&HorcruxHeader{
68			OriginalFilename: originalFilename,
69			Timestamp:        timestamp,
70			Index:            index,
71			Total:            total,
72			KeyFragment:      keyFragments[i],
73			Threshold:        threshold,
74		})
75		if err != nil {
76			return err
77		}
78
79		originalFilenameWithoutExt := strings.TrimSuffix(originalFilename, filepath.Ext(originalFilename))
80		horcruxFilename := fmt.Sprintf("%s_%d_of_%d.horcrux", originalFilenameWithoutExt, index, total)
81		horcruxPath := filepath.Join(destination, horcruxFilename)
82		fmt.Printf("creating %s\n", horcruxPath)
83
84		// clearing file in case it already existed
85		_ = os.Truncate(horcruxPath, 0)
86
87		horcruxFile, err := os.OpenFile(horcruxPath, os.O_WRONLY|os.O_CREATE, 0644)
88		if err != nil {
89			return err
90		}
91		defer horcruxFile.Close()
92
93		if _, err := horcruxFile.WriteString(header(index, total, headerBytes)); err != nil {
94			return err
95		}
96
97		horcruxFiles[i] = horcruxFile
98	}
99
100	// wrap file reader in an encryption stream
101	var fileReader io.Reader = file
102	reader := cryptoReader(fileReader, key)
103
104	var writer io.Writer
105	if threshold == total {
106		// because we need all horcruxes to reconstitute the original file,
107		// we'll use a multiplexer to divide the encrypted content evenly between
108		// the horcruxes
109		writer = &multiplexing.Demultiplexer{Writers: horcruxFiles}
110	} else {
111		writers := make([]io.Writer, len(horcruxFiles))
112		for i := range writers {
113			writers[i] = horcruxFiles[i]
114		}
115
116		writer = io.MultiWriter(writers...)
117	}
118
119	_, err = io.Copy(writer, reader)
120	if err != nil {
121		return err
122	}
123
124	fmt.Println("Done!")
125
126	return nil
127}
128
129func obtainTotalAndThreshold() (int, int, error) {
130	totalPtr := flag.Int("n", 0, "number of horcruxes to make")
131	thresholdPtr := flag.Int("t", 0, "number of horcruxes required to resurrect the original file")
132	flag.Parse()
133
134	total := *totalPtr
135	threshold := *thresholdPtr
136
137	if total == 0 {
138		totalStr := Prompt("How many horcruxes do you want to split this file into? (2-99): ")
139		var err error
140		total, err = strconv.Atoi(totalStr)
141		if err != nil {
142			return 0, 0, err
143		}
144	}
145
146	if threshold == 0 {
147		thresholdStr := Prompt("How many horcruxes should be required to reconstitute the original file? If you require all horcruxes, the resulting files will take up less space, but it will feel less magical (2-99): ")
148		var err error
149		threshold, err = strconv.Atoi(thresholdStr)
150		if err != nil {
151			return 0, 0, err
152		}
153	}
154
155	return total, threshold, nil
156}
157
158func header(index int, total int, headerBytes []byte) string {
159	return fmt.Sprintf(`# THIS FILE IS A HORCRUX.
160# IT IS ONE OF %d HORCRUXES THAT EACH CONTAIN PART OF AN ORIGINAL FILE.
161# THIS IS HORCRUX NUMBER %d.
162# IN ORDER TO RESURRECT THIS ORIGINAL FILE YOU MUST FIND THE OTHER %d HORCRUX(ES) AND THEN BIND THEM USING THE PROGRAM FOUND AT THE FOLLOWING URL
163# https://github.com/jesseduffield/horcrux
164
165-- HEADER --
166%s
167-- BODY --
168`, total, index, total-1, headerBytes)
169}
170
171func generateKey() ([]byte, error) {
172	key := make([]byte, 32)
173	_, err := rand.Read(key)
174	return key, err
175}
176