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