Skip to content

Commit 2662c2d

Browse files
feat: encryption cmds (#4)
1 parent 3cec2b5 commit 2662c2d

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed

cmd/encryption/encryption.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package encryption
2+
3+
import (
4+
"bytes"
5+
"crypto/aes"
6+
"crypto/cipher"
7+
"crypto/rand"
8+
"encoding/base64"
9+
"errors"
10+
"fmt"
11+
"io"
12+
"log"
13+
"os"
14+
15+
"github.com/spf13/cobra"
16+
)
17+
18+
const (
19+
AESKeySize256 = 256
20+
21+
secretDirPerm = 0700
22+
keyFilePerm = 0600
23+
bitsToByteRatio = 8
24+
)
25+
26+
var EncryptionCmd = &cobra.Command{
27+
Use: "encryption",
28+
Short: "🔐 Manage data encryption and decryption",
29+
}
30+
31+
func init() {
32+
EncryptionCmd.AddCommand(aesEncryptCommand())
33+
EncryptionCmd.AddCommand(aesDecryptCommand())
34+
EncryptionCmd.AddCommand(aesGenerateKeyCommand())
35+
}
36+
37+
func aesEncryptCommand() *cobra.Command {
38+
var inputPath, outputPath, keyPath string
39+
40+
cmd := &cobra.Command{
41+
Use: "encrypt",
42+
Short: "🔒 Encrypt a file using AES (key is base64-encoded in a file)",
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
fmt.Println("🔐 Starting AES encryption...")
45+
46+
keyB64, err := os.ReadFile(keyPath)
47+
if err != nil {
48+
return fmt.Errorf("❌ Failed to read key file: %w", err)
49+
}
50+
key, err := base64.StdEncoding.DecodeString(string(keyB64))
51+
if err != nil {
52+
return fmt.Errorf("❌ Failed to decode base64 key: %w", err)
53+
}
54+
if len(key) != (AESKeySize256 / bitsToByteRatio) {
55+
return fmt.Errorf("❌ Invalid AES key length: %d bytes", len(key))
56+
}
57+
58+
plaintext, err := os.ReadFile(inputPath)
59+
if err != nil {
60+
return fmt.Errorf("❌ Failed to read input file: %w", err)
61+
}
62+
63+
plaintext = pkcs7Pad(plaintext, aes.BlockSize)
64+
65+
block, err := aes.NewCipher(key)
66+
if err != nil {
67+
return fmt.Errorf("❌ Failed to create cipher: %w", err)
68+
}
69+
70+
iv := make([]byte, aes.BlockSize)
71+
if _, errGenIV := rand.Read(iv); errGenIV != nil {
72+
return fmt.Errorf("❌ Failed to generate IV: %w", errGenIV)
73+
}
74+
75+
ciphertext := make([]byte, len(plaintext))
76+
mode := cipher.NewCBCEncrypter(block, iv)
77+
mode.CryptBlocks(ciphertext, plaintext)
78+
79+
outFile, err := os.Create(outputPath)
80+
if err != nil {
81+
return fmt.Errorf("❌ Failed to create output file: %w", err)
82+
}
83+
defer outFile.Close()
84+
85+
if _, err := outFile.Write(iv); err != nil {
86+
return fmt.Errorf("❌ Failed to write IV: %w", err)
87+
}
88+
if _, err := outFile.Write(ciphertext); err != nil {
89+
return fmt.Errorf("❌ Failed to write ciphertext: %w", err)
90+
}
91+
92+
fmt.Println("✅ File encrypted! 📁 Saved to:", outputPath)
93+
return nil
94+
},
95+
}
96+
97+
cmd.Flags().StringVarP(&inputPath, "in", "i", "", "Plaintext input file path")
98+
cmd.Flags().StringVarP(&outputPath, "out", "o", "", "Encrypted output file path")
99+
cmd.Flags().StringVarP(&keyPath, "key", "k", "", "Base64-encoded AES key file")
100+
101+
if err := cmd.MarkFlagRequired("in"); err != nil {
102+
log.Fatalf("❌ Failed to mark 'in' flag as required: %v", err)
103+
}
104+
105+
if err := cmd.MarkFlagRequired("out"); err != nil {
106+
log.Fatalf("❌ Failed to mark 'out' flag as required: %v", err)
107+
}
108+
109+
if err := cmd.MarkFlagRequired("key"); err != nil {
110+
log.Fatalf("❌ Failed to mark 'key' flag as required: %v", err)
111+
}
112+
113+
return cmd
114+
}
115+
116+
func aesDecryptCommand() *cobra.Command {
117+
var inputPath, outputPath, keyPath string
118+
119+
cmd := &cobra.Command{
120+
Use: "decrypt",
121+
Short: "🔓 Decrypt a file using AES (key is base64-encoded in a file)",
122+
RunE: func(cmd *cobra.Command, args []string) error {
123+
fmt.Println("🔓 Starting AES decryption...")
124+
125+
keyB64, err := os.ReadFile(keyPath)
126+
if err != nil {
127+
return fmt.Errorf("❌ Failed to read key file: %w", err)
128+
}
129+
key, err := base64.StdEncoding.DecodeString(string(keyB64))
130+
if err != nil {
131+
return fmt.Errorf("❌ Failed to decode base64 key: %w", err)
132+
}
133+
if len(key) != (AESKeySize256 / bitsToByteRatio) {
134+
return fmt.Errorf("❌ Invalid AES key length: %d bytes", len(key))
135+
}
136+
137+
inFile, err := os.Open(inputPath)
138+
if err != nil {
139+
return fmt.Errorf("❌ Failed to open input file: %w", err)
140+
}
141+
defer inFile.Close()
142+
143+
iv := make([]byte, aes.BlockSize)
144+
if _, errReadIV := io.ReadFull(inFile, iv); errReadIV != nil {
145+
return fmt.Errorf("❌ Failed to read IV: %w", errReadIV)
146+
}
147+
148+
ciphertext, err := io.ReadAll(inFile)
149+
if err != nil {
150+
return fmt.Errorf("❌ Failed to read ciphertext: %w", err)
151+
}
152+
if len(ciphertext)%aes.BlockSize != 0 {
153+
return errors.New("❌ Ciphertext is not a multiple of the block size")
154+
}
155+
156+
block, err := aes.NewCipher(key)
157+
if err != nil {
158+
return fmt.Errorf("❌ Failed to create cipher: %w", err)
159+
}
160+
161+
mode := cipher.NewCBCDecrypter(block, iv)
162+
plaintext := make([]byte, len(ciphertext))
163+
mode.CryptBlocks(plaintext, ciphertext)
164+
165+
plaintext, err = pkcs7Unpad(plaintext)
166+
if err != nil {
167+
return fmt.Errorf("❌ Failed to unpad plaintext: %w", err)
168+
}
169+
170+
if err := os.WriteFile(outputPath, plaintext, keyFilePerm); err != nil {
171+
return fmt.Errorf("❌ Failed to write output file: %w", err)
172+
}
173+
174+
fmt.Println("✅ File decrypted! 📁 Saved to:", outputPath)
175+
return nil
176+
},
177+
}
178+
179+
cmd.Flags().StringVarP(&inputPath, "in", "i", "", "Encrypted input file path")
180+
cmd.Flags().StringVarP(&outputPath, "out", "o", "", "Decrypted output file path")
181+
cmd.Flags().StringVarP(&keyPath, "key", "k", "", "Base64-encoded AES key file")
182+
183+
if err := cmd.MarkFlagRequired("in"); err != nil {
184+
log.Fatalf("❌ Failed to mark 'in' flag as required: %v", err)
185+
}
186+
187+
if err := cmd.MarkFlagRequired("out"); err != nil {
188+
log.Fatalf("❌ Failed to mark 'out' flag as required: %v", err)
189+
}
190+
191+
if err := cmd.MarkFlagRequired("key"); err != nil {
192+
log.Fatalf("❌ Failed to mark 'key' flag as required: %v", err)
193+
}
194+
195+
return cmd
196+
}
197+
198+
func aesGenerateKeyCommand() *cobra.Command {
199+
var output string
200+
201+
cmd := &cobra.Command{
202+
Use: "genkey",
203+
Short: "🔑 Generate a random AES key and store it in base64 in .secrets/",
204+
RunE: func(cmd *cobra.Command, args []string) error {
205+
keyLen := AESKeySize256 / bitsToByteRatio
206+
key := make([]byte, keyLen)
207+
if _, err := rand.Read(key); err != nil {
208+
return fmt.Errorf("❌ Failed to generate key: %w", err)
209+
}
210+
211+
b64Key := base64.StdEncoding.EncodeToString(key)
212+
213+
if output == "" {
214+
if err := os.MkdirAll(".secrets", secretDirPerm); err != nil {
215+
return fmt.Errorf("❌ Failed to create secrets directory: %w", err)
216+
}
217+
output = fmt.Sprintf(".secrets/aes-key-%d.b64", AESKeySize256)
218+
}
219+
220+
if err := os.WriteFile(output, []byte(b64Key), keyFilePerm); err != nil {
221+
return fmt.Errorf("❌ Failed to write key file: %w", err)
222+
}
223+
224+
fmt.Printf("✅ AES-%d key generated and saved to %s\n", AESKeySize256, output)
225+
return nil
226+
},
227+
}
228+
229+
cmd.Flags().StringVarP(&output, "out", "o", "", "Output file path (default: .secrets/aes-key-<size>.b64)")
230+
231+
return cmd
232+
}
233+
234+
func pkcs7Pad(data []byte, blockSize int) []byte {
235+
padding := blockSize - len(data)%blockSize
236+
padText := bytes.Repeat([]byte{byte(padding)}, padding)
237+
238+
return append(data, padText...)
239+
}
240+
241+
func pkcs7Unpad(data []byte) ([]byte, error) {
242+
if len(data) == 0 {
243+
return nil, errors.New("invalid padding size")
244+
}
245+
paddingLen := int(data[len(data)-1])
246+
247+
if paddingLen == 0 || paddingLen > len(data) {
248+
return nil, errors.New("invalid padding")
249+
}
250+
251+
return data[:len(data)-paddingLen], nil
252+
}

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"github.com/thewhitewizard/web3data-cli/cmd/encryption"
45
"github.com/thewhitewizard/web3data-cli/cmd/ipfs"
56
"github.com/thewhitewizard/web3data-cli/cmd/version"
67

@@ -17,6 +18,7 @@ func Execute() {
1718
}
1819

1920
func init() {
21+
rootCmd.AddCommand(encryption.EncryptionCmd)
2022
rootCmd.AddCommand(ipfs.IPFSCmd)
2123
rootCmd.AddCommand(version.VersionCmd)
2224
}

0 commit comments

Comments
 (0)