Files
vault-unwrap/vault.go

574 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// -------------------------------------------
// Hashicorp Vault wrap/unwrap web service
// Distributed under GNU Public License
// Author: Sergey Kalinin svk@nuk-svk.ru
// Home page: https://nuk-svk.ru https://git.nuk-svk.ru
// -------------------------------------------
package main
import (
// "context"
"log"
// "time"
"io"
"os"
"fmt"
"flag"
"regexp"
"bytes"
"strconv"
"strings"
"time"
"encoding/json"
"encoding/base64"
"crypto/tls"
"net/http"
"html/template"
"path/filepath"
"github.com/gorilla/mux"
"github.com/sethvargo/go-password/password"
)
// {
// "request_id": "540486b5-80b6-4250-1ba3-ec562984c58c",
// "lease_id": "",
// "renewable": false,
// "lease_duration": 0,
// "data": {
// "user": "password"
// },
// "wrap_info": null,
// "warnings": null,
// "auth": null,
// "mount_type": "system"
// }
//
var (
Debug bool
TemplateDir string
TemplateFile string
ActionAddress string
VaultAddress string
VaultToken string
Data string
ListenPort string
TlsEnable bool
TlsCertFile string
TlsKeyFile string
MaxTextLength int
TokenTTL string
)
type TemplateData struct {
URL string
TEXT string
MAXTEXTLENGTH int
TOKENTTL string
}
type UnwrappedData struct {
Rerquest_id string `json: "request_id"`
Lease_id string `json: "lease_id"`
Renewable bool `json: "renewable"`
Lease_daration int `json:"lease_duration"`
Data map[string]string `json: "data"`
Wrap_info *WrapInfo `json: "wrap_info"`
Warnings string `json: "warnings"`
Auth string `json: "auth"`
Mount_type string `json: "mount_type"`
Errors string `json: "errors"`
}
type WrapInfo struct {
Token string `json:"token"`
Ttl int `json:"ttl"`
CreationTime time.Time `json:"creation_time"`
CreationPath string `json:"creation_path"`
}
type FileViewerData struct {
Title string
Base64Data string
MimeType string
FileSize string
FileName string
IsImage bool
}
// const MaxTextLength = 100
func vaultDataWrap(vaultAddr string, dataType string, content string) string {
secret := make(map[string]string)
// switch dataType {
// case "text":
// secret["wrapped_data"] = text
// case "file":
// secret["file"] = text
// default:
// secret["wrapped_data"] = text
// }
// Определяем ключ для данных
if Debug {
log.Println("Тип данныж:", dataType)
}
if dataType == "text" {
// Для текста используем ключ "text"
secret["text"] = content
} else {
// Для файла используем имя файла как ключ
// Если dataType не "text" и не пустое, считаем его именем файла
if dataType == "" {
dataType = "file" // fallback, если имя не указано
}
secret[dataType] = content
}
postBody, err := json.Marshal(secret)
if err != nil {
log.Println("Errored marshaling the text:", content)
}
customTransport := &(*http.DefaultTransport.(*http.Transport))
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{Transport: customTransport}
// client := &http.Client{}
req, _ := http.NewRequest("POST", vaultAddr, bytes.NewBuffer(postBody))
req.Header.Add("Accept", "application/json")
req.Header.Add("X-Vault-Token", VaultToken)
req.Header.Add("X-Vault-Wrap-TTL", TokenTTL)
resp, err := client.Do(req)
if err != nil {
log.Println("Errored when sending request to the Vault server")
}
var result UnwrappedData
json.NewDecoder(resp.Body).Decode(&result)
// wrappedToken := result.Data
if Debug {
log.Println(resp)
log.Println("result", result)
log.Println("token", result.Wrap_info.Token)
}
return result.Wrap_info.Token
}
func vaultDataUnWrap(vaultAddr string, vaultWrapToken string) map[string]string {
log.Printf("Vault address: %s ", vaultAddr)
customTransport := &(*http.DefaultTransport.(*http.Transport))
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{Transport: customTransport}
// client := &http.Client{}
log.Println(vaultAddr, vaultWrapToken)
req, _ := http.NewRequest("POST", vaultAddr, nil)
req.Header.Add("Accept", "application/json")
req.Header.Add("X-Vault-Token", vaultWrapToken)
resp, err := client.Do(req)
if err != nil {
log.Println("Errored when sending request to the Vault server", err)
}
if Debug {
log.Println(resp)
}
var result UnwrappedData
json.NewDecoder(resp.Body).Decode(&result)
secret := result.Data
if Debug {
log.Println("result", result)
log.Println("secret", secret)
}
// log.Println("Length=", len(secret))
// if len(secret) == 0 {
// log.Println("Error:", result.Errors)
// }
// fmt.Sprint(secret)
// for v, k := range secret {
// log.Println(k, v)
// }
return secret
}
func ParseTemplate(templateFileName string, data interface{}) (body string, err error) {
body = ""
t, err := template.ParseFiles(templateFileName)
if err != nil {
log.Println("Ошибка преобразования html шаблона", templateFileName, err)
return
}
buf := new(bytes.Buffer)
if err = t.Execute(buf, data); err != nil {
log.Println("Ошибка преобразования html шаблона", templateFileName, err)
body = ""
} else {
body = buf.String()
}
return
}
func getStaticPage(w http.ResponseWriter, r *http.Request) {
var (
templateData TemplateData
)
template := filepath.Join(TemplateDir, TemplateFile)
// templateData.UUID = uuid
templateData.URL = ActionAddress
templateData.TEXT = Data
templateData.MAXTEXTLENGTH = MaxTextLength
templateData.TOKENTTL = TokenTTL
// templateData.URL = FishingUrl + "/" + arrUsers[i].messageUUID
if body, err := ParseTemplate(template, templateData); err == nil {
w.Write([]byte(body))
}
}
func showFileViewer(w http.ResponseWriter, r *http.Request, fileData []byte, fileName string) {
// Определяем MIME-тип файла
mimeType := http.DetectContentType(fileData)
isImage := strings.HasPrefix(mimeType, "image/")
// Кодируем в base64
base64Data := base64.StdEncoding.EncodeToString(fileData)
// Подготавливаем данные для шаблона
data := FileViewerData{
Title: "Просмотр файла",
Base64Data: base64Data,
MimeType: mimeType,
FileSize: fmt.Sprintf("%.2f", float64(len(fileData))/1024),
FileName: fileName,
IsImage: isImage,
}
// Парсим шаблон
tmpl, err := template.ParseFiles(filepath.Join(TemplateDir, "file-viewer.html"))
if err != nil {
http.Error(w, "Error loading template", http.StatusInternalServerError)
return
}
// Отображаем шаблон
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Error rendering template", http.StatusInternalServerError)
}
}
// hvs.CAES - 95
// s.Dj7kZS - 26
func unwrapDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
token := r.FormValue("input_token")
vaultPath := VaultAddress + "/v1/sys/wrapping/unwrap"
Data = ""
// Нормализация токена
re := regexp.MustCompile(`^(hvs|s)\.[\w\-\_]+`)
token = strings.TrimSpace(strings.ReplaceAll(token, "\r\n", ""))
if token != "" && re.MatchString(token) {
unwrappedData := vaultDataUnWrap(vaultPath, token)
// 1. Сначала проверяем наличие текстовых данных
if textData, exists := unwrappedData["text"]; exists {
Data = textData
} else {
// 2. Если текста нет, ищем файловые данные (любой ключ, кроме "text")
var fileData []byte
var fileName string
for key, value := range unwrappedData {
if key != "text" { // Пропускаем текстовые данные, если они есть
// Декодируем base64
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
log.Printf("Ошибка декодирования файла %s: %v", key, err)
continue
}
fileData = decoded
fileName = key
break
}
}
if fileData != nil {
// Если нашли файл - показываем просмотрщик
showFileViewer(w, r, fileData, fileName)
return
} else {
// Если не нашли ни текста, ни файла - выводим все данные как текст
b := new(bytes.Buffer)
for key, value := range unwrappedData {
fmt.Fprintf(b, "%s: %s\n", key, value)
}
Data = b.String()
}
}
if Data == "" {
Data = "Ошибка! Токен не содержит данных."
}
} else if token != "" {
Data = "Введенные данные не соответствуют формату. Введите корректный токен."
}
getStaticPage(w, r)
}
func genPassword(w http.ResponseWriter, r *http.Request) {
// params := mux.Vars(r)
// passLength := params["passLength"]
Data = ""
r.ParseForm()
passLength := r.FormValue("passlength")
if Debug {
log.Printf(r.FormValue("passlength"), passLength)
}
if len(passLength) == 0 {
passLength = "32"
}
// w.Write([]byte("Длина пароля " + passLength + "/n"))
passwordLength, err := strconv.Atoi(passLength)
if passwordLength > MaxTextLength {
log.Printf("Oversized password length")
Data = "Превышена длина пароля"
getStaticPage(w, r)
return
}
if err != nil {
log.Println(err)
}
res, err := password.Generate(passwordLength, 5, 5, false, true)
if err != nil {
log.Println(err)
}
if Debug {
log.Printf(res)
}
Data = res
// w.Write([]byte(res))
getStaticPage(w, r)
}
func genPasswordDefault(w http.ResponseWriter, r *http.Request) {
res, err := password.Generate(64, 10, 5, false, false)
if err != nil {
log.Fatal(err)
}
log.Printf(res)
// w.Write([]byte(res))
Data = res
// w.Write([]byte(res))
getStaticPage(w, r)
}
func wrapDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
secret := r.FormValue("input_token")
if len([]rune(secret)) > MaxTextLength {
log.Println("Длина текста превышает заданные ограничения:", MaxTextLength)
Data = fmt.Sprintf("Длина текста превышает заданные ограничения: %s > %s", strconv.Itoa(len([]rune(secret))), strconv.Itoa(MaxTextLength))
} else {
vaultPath := VaultAddress + "/v1/sys/wrapping/wrap"
Data = ""
if Debug {
fmt.Println("Введен текст:", secret)
}
if secret != "" {
Data = vaultDataWrap(vaultPath, "text", secret)
if Data == "" {
Data = "Ошибка! Токен не найден."
}
if Debug {
log.Println(Data)
}
// Переводим секунды в часы и дабавляем к токену для информации.
ttl, _ := strconv.Atoi(TokenTTL)
ttl = ttl / 3600
Data = fmt.Sprintf("%s\n\n---\nВремя жизни токена %s ч.", Data, strconv.Itoa(ttl))
} else if secret != "" {
Data = "Введите текст для шифровки."
}
}
getStaticPage(w, r)
}
func wrapDataFromFile(w http.ResponseWriter, r *http.Request) {
MaxFileLength := 100000 // Максимальный размер файла (100KB)
// 1. Проверяем метод (должен быть POST)
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 2. Проверяем Content-Type (должен быть multipart/form-data)
contentType := r.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "multipart/form-data") {
http.Error(w, "Expected multipart/form-data", http.StatusBadRequest)
return
}
// 3. Парсим форму с ограничением размера (10 МБ)
maxMemory := 10 << 20 // 10 MB
if err := r.ParseMultipartForm(int64(maxMemory)); err != nil {
log.Printf("Failed to parse multipart form: %v", err)
http.Error(w, "Unable to parse form (file too big?)", http.StatusBadRequest)
return
}
// 4. Получаем файл из формы (включая метаданные)
file, fileHeader, err := r.FormFile("file")
if err != nil {
log.Printf("Failed to get file from form: %v", err)
http.Error(w, "Error retrieving the file", http.StatusBadRequest)
return
}
defer file.Close()
// Получаем оригинальное имя файла
fileName := fileHeader.Filename
if fileName == "" {
fileName = "unnamed_file" // Запасной вариант, если имя не указано
}
if Debug {
log.Printf("Processing file: %s (size: %d bytes)", fileName, fileHeader.Size)
}
// 5. Читаем содержимое файла
fileBytes, err := io.ReadAll(file)
if err != nil {
log.Printf("Failed to read file: %v", err)
http.Error(w, "Error reading the file", http.StatusInternalServerError)
return
}
// 6. Проверяем размер файла
if len(fileBytes) > MaxFileLength {
errMsg := fmt.Sprintf("File too large (%d > %d)", len(fileBytes), MaxFileLength)
http.Error(w, errMsg, http.StatusBadRequest)
return
}
// 7. Кодируем в base64
encoded := base64.StdEncoding.EncodeToString(fileBytes)
// 8. Обёртываем данные в Vault с именем файла
vaultPath := VaultAddress + "/v1/sys/wrapping/wrap"
token := vaultDataWrap(vaultPath, fileName, encoded)
if token == "" {
http.Error(w, "Failed to wrap data in Vault", http.StatusInternalServerError)
return
}
ttl, _ := strconv.Atoi(TokenTTL)
ttl = ttl / 3600
Data = fmt.Sprintf("%s\n\n---\nВремя жизни токена %s ч.", token, strconv.Itoa(ttl))
getStaticPage(w, r)
}
func main() {
var (
logFile string
)
flag.BoolVar(&Debug, "debug", false, "Вывод отладочных сообщений в консоль")
flag.StringVar(&logFile, "log-file", "vault-unwrap.log", "Путь до лог-файла ")
flag.StringVar(&TemplateDir, "template-dir", "html-template", "Каталог с шаблонами")
flag.StringVar(&TemplateFile, "template-file", "index.html", "Файл-шаблон для ВЭБ-странцы")
flag.StringVar(&VaultAddress, "vault-url", "", "Адрес сервера Hashicorp Vault (https://host.name:8200)")
flag.StringVar(&ActionAddress, "action-address", "", "Адрес данного сервиса (https://host.name)")
flag.StringVar(&ListenPort, "listen-port", "8080", "Номер порта сервиса")
flag.StringVar(&TlsCertFile, "tls-cert", "", "TLS сертификат (файл)")
flag.StringVar(&TlsKeyFile, "tls-key", "", "TLS ключ (файл)")
flag.BoolVar(&TlsEnable, "tls", false, "Использовать SSL/TLS")
flag.IntVar(&MaxTextLength, "max-text-length", 100 , "Максимальная длина текста для шифрования и длина пароля для генератора")
flag.StringVar(&TokenTTL, "token-ttl", "3600", "Время жизни wrap-токена в секундах")
flag.Parse()
if os.Getenv("logFile") != "" {
logFile = os.Getenv("logFile")
}
fLog, err := os.OpenFile(logFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer fLog.Close()
if Debug {
log.SetOutput(os.Stdout)
} else {
log.SetOutput(fLog)
}
if os.Getenv("VAULT_ADDRESS") == "" && VaultAddress == "" {
log.Println("Send error: make sure environment variables `VAULT_ADDRESS` was set")
} else if os.Getenv("VAULT_ADDRESS") != "" && VaultAddress == "" {
VaultAddress = os.Getenv("VAULT_ADDRESS")
}
if os.Getenv("VAULT_TOKEN") == "" && VaultToken == "" {
log.Println("Send error: make sure environment variables `VAULT_TOKEN` was set")
} else if os.Getenv("VAULT_TOKEN") != "" && VaultToken == "" {
VaultToken = os.Getenv("VAULT_TOKEN")
}
if os.Getenv("MAX_TEXT_LENGTH") != "" && MaxTextLength == 100 {
MaxTextLength, err = strconv.Atoi(os.Getenv("MAX_TEXT_LENGTH"))
if err != nil {
log.Printf("Ошибка преобразования значения ", os.Getenv("MAX_TEXT_LENGTH"))
}
}
if Debug {
log.Printf("Адрес сервера Hashicorp Vault: %s ", VaultAddress)
}
rtr := mux.NewRouter()
rtr.HandleFunc("/unwrap", unwrapDataFromHtmlForm)
rtr.HandleFunc("/wrap", wrapDataFromHtmlForm)
rtr.HandleFunc("/wrapfile", wrapDataFromFile).Methods("POST")
rtr.HandleFunc("/genpassword/{passLength:[0-9]+}", genPassword)
rtr.HandleFunc("/genpassword", genPassword)
rtr.HandleFunc("/", unwrapDataFromHtmlForm)
rtr.PathPrefix("/").Handler(http.FileServer(http.Dir("./static")))
http.Handle("/", rtr)
if os.Getenv("LISTEN_PORT") != "" {
ListenPort = os.Getenv("LISTEN_PORT")
} else {
if TlsEnable && ListenPort == ""{
ListenPort = "8443"
}
}
listenAddr := ":" + ListenPort
// ActionAddress = "https://" + ActionAddress
if Debug {
log.Printf("Адрес сервиса: %s%s ", ActionAddress, listenAddr)
}
log.Println("Listening...")
if TlsEnable {
log.Fatal(http.ListenAndServeTLS(listenAddr, TlsCertFile, TlsKeyFile, nil))
} else {
http.ListenAndServe(listenAddr, nil)
}
}