Compare commits

..

5 Commits

Author SHA1 Message Date
Калинин Сергей Валерьевич
c4ded33507 Добавлена работа с файлами. Добавлен показ изображений и загрузка файлов. Изменен дизайн 2025-07-21 13:10:19 +03:00
svk
10c9635894 Обновить vault.go 2024-10-28 16:09:39 +03:00
Калинин Сергей Валерьевич
7ecdf7a606 Добавлена возможность шифровать данные (wrap).
Добавлена установка времени жизни токена (TTL).
Добавлено соответствие ограничения длины пароля и длины текста для шифрования.
Добавлена подготовка токена к расшифровке (ограничение длины, удаление пробельных символов).
2024-10-28 16:03:45 +03:00
svk
41e1a553f6 Обновить README.md 2024-10-17 15:08:06 +03:00
Калинин Сергей Валерьевич
897bb3de01 Добавлена поддержка wrap 2024-10-17 15:04:35 +03:00
7 changed files with 680 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
# Vault Wrap/Unwrap
ВЭБ-интерфейс к сервису vault/unwrap для безопасной передачи секретов. Также генератор паролей.
ВЭБ-интерфейс к сервису Hashicorp Vault wrap/unwrap для безопасной передачи секретов. Также генератор паролей.
## Запуск
@@ -14,23 +14,26 @@ vault-wrap -action-address "https://secret.example.ru:8443" -vault-url "https://
Запуск с доступом по http:
```
vault-wrap -action-address "http://saecret.example.ru:8080" -vault-url "https://vault.example.ru:8200" -tls-cert cert.pem -tls-key privaty.key -listen-port 8080
vault-wrap -action-address "http://secret.example.ru:8080" -vault-url "https://vault.example.ru:8200" -tls-cert cert.pem -tls-key privaty.key -listen-port 8080
```
Если сервис запущен за обратным прокси, то в качестве адреса сервиса -action-address требуется указать адрес прокси, т.е. адрес (FQDN) на который будут приходить запросы.
Для работы шифрования (wrap) нужен vault токен с доступом к vault wrap/unwrap. Токен задается через переменную окружения VAULT_TOKEN.
## Ключи командной строки
- action-address string - Адрес данного сервиса (https://secret.example.ru). Адрес который будет подставляться в форму в html-шаблон
- debug - Вывод отладочных сообщений
- listen-port string - Номер порта сервиса (default "8080")
- log-file string - Путь до лог-файла (default "vault-unwrap.log")
- max-text-length - Максимальная длина текста для шифрования и длина пароля для генератора (default 100)
- template-dir string -Каталог с шаблонами (default "html-template")
- template-file string Файл-шаблон для ВЭБ-странцы (default "index.html")
- tls - Использовать SSL/TLS
- tls-cert string - TLS сертификат (полный путь к файлу)
- tls-key string - TLS ключ (полный путь к файлу)
- token-ttl - Время жизни wrap-токена в секундах (default "3600")
- vault-url string - Адрес сервера Hashicorp Vault (https://host.name:8200)
- help - Вывод справочной информации

View File

@@ -8,10 +8,13 @@ services:
environment:
- ACTION_ADDRESS=${ACTION_ADDRESS:-https://secret.example.ru}
- VAULT_ADDRESS=${VAULT_ADDRESS}
- VAULT_TOKEN=${WRAP_TOKEN}
- LISTEN_PORT=8080
- TLS_KEY_FILE=${TLS_KEY_FILE}
- TLS_CERT_FILE=${TLS_CERT_FILE}
- TZ=Europe/Moscow
- MAX_TEXT_LENGTH=${MAX_TEXT_LENGTH:-100}
- TOKEN_TTL=${TOKEN_TTL:-3600}
restart: always
# ports:
# - 1234:8080
@@ -26,16 +29,48 @@ services:
max-size: "10m"
max-file: "5"
labels:
- "tra.enable=true"
- "tra.http.routers.secret.rule=Host(`secret.example.ru`)"
- "tra.http.services.secret.loadbalancer.server.port=8080"
- "tra.docker.network=reverse-proxy"
- "tra.http.routers.secret.tls=true"
- "tra.http.services.secret.loadbalancer.server.scheme=http"
- "traefik.enable=true"
- "traefik.http.routers.secret.rule=Host(`secret.example.ru`)"
- "traefik.http.services.secret.loadbalancer.server.port=8080"
- "traefik.docker.network=reverse-proxy"
- "traefik.http.routers.secret.tls=true"
- "traefik.http.services.secret.loadbalancer.server.scheme=http"
networks:
- default
- vault-wrap
traefik:
image: traefik:v3.0
container_name: traefik
command:
# - --entrypoints.web.address=:80
# - --entrypoints.web-secure.address=:443
# - --providers.docker=true
- --providers.file.directory=/configuration/
- --providers.file.watch=true
volumes:
- traefik-dynamic-conf:/configuration/
- /home/gitlab-runner/traefik/traefik.yml:/traefik.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-ssl:/ssl/:ro
ports:
- 80:80
# - 8080:8080
- 888:888
- 443:443
restart: always
networks:
- default
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.rule=Host(`example.ru`)"
- "traefik.http.routers.traefik.tls=true"
# - "traefik.http.routers.traefik.tls.certresolver=letsEncrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.services.traefik.loadbalancer.server.port=888"
- "traefik.http.services.traefik.loadbalancer.server.scheme=https"
networks:
default:
name: reverse-proxy
@@ -46,3 +81,5 @@ networks:
volumes:
vault-wrap-log:
vault-wrap-conf:
traefik-dynamic-conf:
traefik-ssl:

View File

@@ -3,6 +3,6 @@ set -u
while true ;do
# /go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -vault-url "${VAULT_ADDRESS}" -tls-cert "/usr/local/share/vault-wrap/${TLS_CERT_FILE}" -tls-key "/usr/local/share/vault-wrap/${TLS_KEY_FILE}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log -listen-port "${LISTEN_PORT}" -tls
/go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log
/go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log -token-ttl ${TOKEN_TTL}
sleep 120
done

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}
.file-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
img {
max-width: 100%;
height: auto;
margin: 20px 0;
}
.file-info {
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 5px;
}
.download-btn {
display: inline-block;
padding: 10px 20px;
background: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 10px;
}
.action-btn {
display: inline-block;
padding: 10px 20px;
background: #f0f0f0;
color: #333;
text-decoration: none;
border-radius: 5px;
margin: 10px;
}
</style>
</head>
<body>
<div class="file-container">
<h1>{{.Title}}</h1>
{{if .IsImage}}
<img src="data:{{.MimeType}};base64,{{.Base64Data}}" alt="File preview">
{{else}}
<div class="file-info">
<p>Этот файл не может быть отображен в браузере</p>
</div>
{{end}}
<div class="file-info">
<p><strong>Тип файла:</strong> {{.MimeType}}</p>
<p><strong>Размер:</strong> {{.FileSize}} KB</p>
<p><strong>Имя файла:</strong> {{.FileName}}</p>
<a href="data:{{.MimeType}};base64,{{.Base64Data}}"
class="download-btn"
download="{{.FileName}}">
Скачать файл
</a>
<a href="/" class="action-btn">На главную</a>
<a href="javascript:history.back()" class="action-btn">Назад</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Unwrap Form</title>
<style>
textarea { width: 100%; max-width: 500px; }
table { border-collapse: collapse; }
hr { margin: 15px 0; }
</style>
</head>
<body>
<table>
<!-- Форма для wrap/unwrap текста -->
<tr><td>
<form method="post" action="{{.URL}}/wrap">
<table>
<tr><td>Введите текст или токен:</td></tr>
<tr><td>
<textarea name="input_token" cols="50" rows="10" maxlength="{{.MAXTEXTLENGTH}}">{{.TEXT}}</textarea>
</td></tr>
<tr><td align="right">
<button type="submit">Зашифровать</button>
<button type="submit" formaction="{{.URL}}/unwrap">Расшифровать</button>
</td></tr>
</table>
</form>
</td></tr>
<tr><td><hr></td></tr>
<!-- Отдельная форма для загрузки файла -->
<tr><td>
<form method="post" action="{{.URL}}/wrapfile" enctype="multipart/form-data">
<input type="file" name="file" id="file" required>
<button type="submit">Зашифровать файл</button>
</form>
</td></tr>
<tr><td><hr></td></tr>
<!-- Форма для генерации пароля -->
<tr><td>
<form method="post" action="{{.URL}}/genpassword">
<table>
<tr><td align="right">
Длина пароля (от 15 до {{.MAXTEXTLENGTH}}):
<input type="number" name="passlength" min="15" max="{{.MAXTEXTLENGTH}}" value="32">
<button type="submit">Сгенерировать пароль</button>
</td></tr>
</table>
</form>
</td></tr>
</table>
</body>
</html>

View File

@@ -1,38 +1,206 @@
<!DOCTYPE html>
<html>
<head>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Unwrap Form</title>
</head>
<body>
<table>
<tr><td>
<!-- <a href={{.URL}}/unwrap>Расшифровать</a> |
<a href={{.URL}}/genpassword>Сгенерировать пароль</a>-->
<tr><td><p></p></td></tr>
<tr><td>
<form method="post">
<table>
<tr><td>
Введите токен:
</td></tr>
<tr><td>
<textarea id="wrapped_token" name="input_token" cols=50 rows=10>{{ .TEXT }}</textarea>
</td></tr>
<tr><td align=right>
<title>Data Wrap/Unwrap Form</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
display: flex;
flex-direction: column;
min-height: 100vh;
box-sizing: border-box; /* Добавлено */
}
.content {
flex: 1;
padding-bottom: 20px; /* Добавлено */
}
.form-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
margin-bottom: 15px; /* Уменьшено */
}
h1 {
color: #333;
text-align: center;
margin-bottom: 25px; /* Уменьшено */
font-size: 1.8em; /* Добавлено */
}
textarea {
width: 100%;
max-width: 100%;
min-height: 120px; /* Уменьшено */
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
resize: vertical;
box-sizing: border-box; /* Добавлено */
}
input[type="file"] {
margin: 8px 0; /* Уменьшено */
width: 100%; /* Добавлено */
}
input[type="number"] {
width: 60px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 4px; /* Уменьшено */
transition: background 0.3s;
}
button:hover {
background: #45a049;
}
hr {
border: 0;
height: 1px;
background: #ddd;
margin: 15px 0; /* Уменьшено */
}
.form-title {
font-weight: bold;
margin-bottom: 8px; /* Уменьшено */
color: #555;
font-size: 0.95em; /* Добавлено */
}
.button-group {
text-align: right;
margin-top: 8px; /* Уменьшено */
}
footer {
text-align: center;
padding: 12px 0;
color: #777;
font-size: 0.85em; /* Уменьшено */
border-top: 1px solid #eee;
background: white;
margin-top: auto; /* Важно для прижатия */
position: sticky;
bottom: 0;
}
.footer-links {
margin-top: 6px; /* Уменьшено */
}
.footer-links a {
color: #4CAF50;
text-decoration: none;
margin: 0 6px; /* Уменьшено */
font-size: 0.85em; /* Уменьшено */
}
.footer-links a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
body {
padding: 15px;
}
.form-container {
padding: 15px;
}
h1 {
font-size: 1.5em;
margin-bottom: 20px;
}
button {
padding: 6px 12px;
font-size: 0.9em;
}
.btn-link {
background: #9C27B0;
}
.btn-link:hover {
background: #7B1FA2;
}
}
</style>
</head>
<body>
<div class="content">
<!-- <h1>Сервис передачи секретов</h1> -->
<div class="form-container">
<div class="form-title">Введите текст или токен:</div>
<form method="post" action="{{.URL}}/wrap">
<textarea id="tokenTextarea" name="input_token" maxlength="{{.MAXTEXTLENGTH}}" placeholder="Введите текст или токен...">{{.TEXT}}</textarea>
<div class="button-group">
<button type="submit">Зашифровать</button>
<button type="submit" formaction="{{.URL}}/unwrap">Расшифровать</button>
<tr><td><hr></td></tr>
</td></tr>
<tr><td align=right>
Длина пароля (от 15 до 1024)
<input type="text" name="passlength"/ size=4 pattern="[0-9]{2,4}">
<button type="submit" formaction="{{.URL}}/genpassword">Сгенерировать пароль</button>
</td></tr>
<button type="button" class="btn-copy" onclick="copyToken()">Скопировать</button>
</div>
</form>
</td></tr>
<tr><td>
</td></tr>
</table>
</body>
</div>
<div class="form-container">
<div class="form-title">Выберите файл (до 100кб):</div>
<form method="post" action="{{.URL}}/wrapfile" enctype="multipart/form-data">
<input type="file" name="file" id="file" required>
<div class="button-group">
<button type="submit">Зашифровать файл</button>
</div>
</form>
</div>
<div class="form-container">
<div class="form-title">Генерация пароля:</div>
<form method="post" action="{{.URL}}/genpassword">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;">
<div style="margin-bottom: 8px;">
Длина пароля (от 15 до {{.MAXTEXTLENGTH}}):
<input type="number" name="passlength" min="15" max="{{.MAXTEXTLENGTH}}" value="32">
</div>
<button type="submit" style="margin-left: auto;">Сгенерировать пароль</button>
</div>
</form>
</div>
</div>
<footer>
<div>© <script>document.write(new Date().getFullYear())</script> SVK</div>
</footer>
<script>
function copyToken() {
const textarea = document.getElementById('tokenTextarea');
const fullText = textarea.value;
const token = fullText.split('\n')[0].trim();
copyToClipboard(token, '.btn-copy');
}
function copyToClipboard(text, buttonSelector) {
const tempInput = document.createElement('input');
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
const copyBtn = document.querySelector(buttonSelector);
const originalText = copyBtn.textContent;
copyBtn.textContent = 'Скопировано!';
setTimeout(() => {
copyBtn.textContent = originalText;
}, 2000);
}
</script>
</body>
</html>

320
vault.go
View File

@@ -1,16 +1,27 @@
// -------------------------------------------
// 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"
@@ -41,15 +52,20 @@ var (
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"`
@@ -57,14 +73,60 @@ type UnwrappedData struct {
Renewable bool `json: "renewable"`
Lease_daration int `json:"lease_duration"`
Data map[string]string `json: "data"`
Wrap_info string `json: "wrap_info"`
Wrap_info *WrapInfo `json: "wrap_info"`
Warnings string `json: "warnings"`
Auth string `json: "auth"`
Mount_type string `json: "mount_type"`
Error string `json: "errors"`
Errors string `json: "errors"`
}
func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string) string {
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}
@@ -72,9 +134,10 @@ func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string)
client := &http.Client{Transport: customTransport}
// client := &http.Client{}
req, _ := http.NewRequest("POST", vaultAddr, nil)
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-Token", VaultToken)
req.Header.Add("X-Vault-Wrap-TTL", TokenTTL)
resp, err := client.Do(req)
@@ -82,11 +145,16 @@ func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string)
log.Println("Errored when sending request to the Vault server")
}
var result map[string]interface{}
var result UnwrappedData
json.NewDecoder(resp.Body).Decode(&result)
secret := result["data"].(map[string]interface{})["data"].(map[string]interface{})[vaultSecretName]
log.Println(result)
return fmt.Sprint(secret)
// 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 {
@@ -115,9 +183,13 @@ func vaultDataUnWrap(vaultAddr string, vaultWrapToken string) map[string]string
json.NewDecoder(resp.Body).Decode(&result)
secret := result.Data
if Debug {
log.Println(result)
log.Println(secret)
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)
@@ -151,8 +223,9 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) {
// 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 {
@@ -160,39 +233,100 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) {
}
}
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 getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
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.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
}
if token != "" && re.Match([]byte(token)) {
fileData = decoded
fileName = key
break
}
}
if fileData != nil {
// Если нашли файл - показываем просмотрщик
showFileViewer(w, r, fileData, fileName)
return
} else {
// Если не нашли ни текста, ни файла - выводим все данные как текст
b := new(bytes.Buffer)
for key, value := range vaultDataUnWrap(vaultPath, token) {
for key, value := range unwrappedData {
fmt.Fprintf(b, "%s: %s\n", key, value)
}
Data = b.String()
if Debug {
log.Println(Data)
}
}
if Data == "" {
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) {
@@ -209,7 +343,7 @@ func genPassword(w http.ResponseWriter, r *http.Request) {
}
// w.Write([]byte("Длина пароля " + passLength + "/n"))
passwordLength, err := strconv.Atoi(passLength)
if passwordLength > 1024 {
if passwordLength > MaxTextLength {
log.Printf("Oversized password length")
Data = "Превышена длина пароля"
getStaticPage(w, r)
@@ -218,7 +352,7 @@ func genPassword(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Println(err)
}
res, err := password.Generate(passwordLength, 10, 5, false, true)
res, err := password.Generate(passwordLength, 5, 5, false, true)
if err != nil {
log.Println(err)
}
@@ -242,6 +376,111 @@ func genPasswordDefault(w http.ResponseWriter, r *http.Request) {
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
@@ -256,6 +495,8 @@ func main() {
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()
@@ -281,17 +522,31 @@ func main() {
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", getDataFromHtmlForm)
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("/", getDataFromHtmlForm)
rtr.HandleFunc("/", unwrapDataFromHtmlForm)
rtr.PathPrefix("/").Handler(http.FileServer(http.Dir("./static")))
http.Handle("/", rtr)
@@ -316,4 +571,3 @@ func main() {
http.ListenAndServe(listenAddr, nil)
}
}