417 lines
13 KiB
Go
417 lines
13 KiB
Go
// -------------------------------------------
|
||
// 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"
|
||
"os"
|
||
"fmt"
|
||
"flag"
|
||
"regexp"
|
||
"bytes"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
"encoding/json"
|
||
"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"`
|
||
}
|
||
|
||
// const MaxTextLength = 100
|
||
|
||
func vaultDataWrap(vaultAddr string, text string) string {
|
||
secret := make(map[string]string)
|
||
secret["wrapped_data"] = text
|
||
|
||
postBody, err := json.Marshal(secret)
|
||
if err != nil {
|
||
log.Println("Errored marshaling the text:", text)
|
||
}
|
||
|
||
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))
|
||
}
|
||
}
|
||
|
||
// 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 = ""
|
||
// fmt.Fprintln(w, r.URL.RawQuery)
|
||
// log.Println(w, r.URL.RawQuery)
|
||
if Debug {
|
||
log.Printf("Текст для шифровки: %s ", token)
|
||
log.Printf("Адрес сервера Hashicorp Vault: %s ", vaultPath)
|
||
}
|
||
// Проверка текста на соответствие шаблону
|
||
re := regexp.MustCompile(`^(hvs|s)\.[\w\-\_]+`)
|
||
if Debug {
|
||
fmt.Println(re.Match([]byte(token)))
|
||
}
|
||
|
||
token = strings.ReplaceAll(token, "\r", "")
|
||
token = strings.ReplaceAll(token, "\n", "")
|
||
token = strings.ReplaceAll(token, " ", "")
|
||
token = strings.TrimSpace(token)
|
||
|
||
if token != "" && re.Match([]byte(token)) {
|
||
b := new(bytes.Buffer)
|
||
for key, value := range vaultDataUnWrap(vaultPath, token) {
|
||
fmt.Fprintf(b, "%s: %s\n", key, value)
|
||
}
|
||
Data = b.String()
|
||
if Data == "" {
|
||
Data = "Ошибка! Токен не найден."
|
||
}
|
||
if Debug {
|
||
log.Println(Data)
|
||
}
|
||
} else if token != "" {
|
||
Data = "Введенные данные не соответствуют формату. Введите корректный токен."
|
||
}
|
||
getStaticPage(w, r)
|
||
// http.Redirect(w, r, "http://"+r.Host, http.StatusMovedPermanently)
|
||
}
|
||
|
||
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, 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 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("/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)
|
||
}
|
||
}
|