The codebase that powers boop.cat
boop.cat
1// Copyright 2025 boop.cat
2// Licensed under the Apache License, Version 2.0
3// See LICENSE file for details.
4
5package lib
6
7import (
8 "crypto/aes"
9 "crypto/cipher"
10 "crypto/rand"
11 "crypto/sha256"
12 "encoding/base64"
13 "os"
14 "strings"
15
16 "golang.org/x/crypto/pbkdf2"
17)
18
19const (
20 ivLength = 16
21 authTagLength = 16
22 saltLength = 16
23 keyLength = 32
24)
25
26func getEncryptionKey() []byte {
27 secret := os.Getenv("ENV_ENCRYPTION_SECRET")
28 if secret == "" {
29 return nil
30 }
31
32 saltHash := sha256.Sum256([]byte(secret + ":salt"))
33 salt := saltHash[:saltLength]
34
35 return pbkdf2.Key([]byte(secret), salt, 100000, keyLength, sha256.New)
36}
37
38func IsEncryptionEnabled() bool {
39 return os.Getenv("ENV_ENCRYPTION_SECRET") != ""
40}
41
42func Encrypt(plaintext string) string {
43 if plaintext == "" {
44 return plaintext
45 }
46
47 key := getEncryptionKey()
48 if key == nil {
49
50 return plaintext
51 }
52
53 block, err := aes.NewCipher(key)
54 if err != nil {
55 return plaintext
56 }
57
58 gcm, err := cipher.NewGCM(block)
59 if err != nil {
60 return plaintext
61 }
62
63 iv := make([]byte, gcm.NonceSize())
64 if _, err := rand.Read(iv); err != nil {
65 return plaintext
66 }
67
68 ciphertext := gcm.Seal(nil, iv, []byte(plaintext), nil)
69
70 authTag := ciphertext[len(ciphertext)-authTagLength:]
71 encrypted := ciphertext[:len(ciphertext)-authTagLength]
72
73 return "enc:v1:" +
74 base64.StdEncoding.EncodeToString(iv) + ":" +
75 base64.StdEncoding.EncodeToString(authTag) + ":" +
76 base64.StdEncoding.EncodeToString(encrypted)
77}
78
79func Decrypt(ciphertext string) string {
80 if ciphertext == "" {
81 return ciphertext
82 }
83
84 if !strings.HasPrefix(ciphertext, "enc:v1:") {
85
86 return ciphertext
87 }
88
89 key := getEncryptionKey()
90 if key == nil {
91
92 return ciphertext
93 }
94
95 parts := strings.Split(ciphertext, ":")
96 if len(parts) != 5 {
97 return ciphertext
98 }
99
100 iv, err := base64.StdEncoding.DecodeString(parts[2])
101 if err != nil {
102 return ciphertext
103 }
104
105 authTag, err := base64.StdEncoding.DecodeString(parts[3])
106 if err != nil {
107 return ciphertext
108 }
109
110 encrypted, err := base64.StdEncoding.DecodeString(parts[4])
111 if err != nil {
112 return ciphertext
113 }
114
115 block, err := aes.NewCipher(key)
116 if err != nil {
117 return ciphertext
118 }
119
120 gcm, err := cipher.NewGCM(block)
121 if err != nil {
122 return ciphertext
123 }
124
125 fullCiphertext := append(encrypted, authTag...)
126
127 plaintext, err := gcm.Open(nil, iv, fullCiphertext, nil)
128 if err != nil {
129 return ciphertext
130 }
131
132 return string(plaintext)
133}