feat: rebrand Hemmelig to paste.es for cloudhost.es

- Set Spanish as default language with ephemeral/encrypted privacy focus
- Translate all user-facing strings and legal pages to Spanish
- Replace Norwegian flag with Spanish flag in footer
- Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship
- Rewrite PrivacyPage: zero data collection, ephemeral design emphasis
- Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references
- Update PWA manifest, HTML meta tags, package.json branding
- Rename webhook headers to X-Paste-Event / X-Paste-Signature
- Update API docs title and contact to paste.es / cloudhost.es

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:30:19 +01:00
commit bc9f96cbd4
268 changed files with 45773 additions and 0 deletions

7
cli-go/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Binary
hemmelig
hemmelig-*
# IDE
.idea/
.vscode/

126
cli-go/README.md Normal file
View File

@@ -0,0 +1,126 @@
# Hemmelig CLI (Go)
A standalone Go binary for creating encrypted, self-destructing secrets via [Hemmelig](https://hemmelig.app).
```
_ _ _ _
| | | | ___ _ __ ___ _ __ ___ ___| (_) __ _
| |_| |/ _ \ '_ ` _ \| '_ ` _ \ / _ \ | |/ _` |
| _ | __/ | | | | | | | | | | __/ | | (_| |
|_| |_|\___|_| |_| |_|_| |_| |_|\___|_|_|\__, |
|___/
```
## Installation
### Download Binary
Download the pre-built binary for your platform from the [CLI releases](https://github.com/HemmeligOrg/Hemmelig.app/releases?q=cli-v&expanded=true).
Replace `VERSION` below with the desired version (e.g., `1.0.0`):
#### Linux (amd64)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-linux-amd64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### Linux (arm64)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-linux-arm64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### macOS (Apple Silicon)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-darwin-arm64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### macOS (Intel)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-darwin-amd64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### Windows (PowerShell)
```powershell
$VERSION = "1.0.1"
Invoke-WebRequest -Uri "https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v$VERSION/hemmelig-windows-amd64.exe" -OutFile "hemmelig.exe"
# Move to a directory in your PATH, e.g.:
Move-Item hemmelig.exe "$env:LOCALAPPDATA\Microsoft\WindowsApps\hemmelig.exe"
```
#### Verify Download
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/checksums.txt -o checksums.txt
sha256sum -c checksums.txt --ignore-missing
```
### Build from Source
```bash
go build -o hemmelig .
```
## Usage
```bash
# Create a simple secret
hemmelig "my secret message"
# With a title and custom expiration
hemmelig "API key: sk-1234" -t "Production API Key" -e 7d
# Password protected
hemmelig "sensitive data" -p "mypassword"
# Multiple views allowed
hemmelig "shared config" -v 5
# Pipe from stdin
cat config.json | hemmelig -t "Config file"
# Use a self-hosted instance
hemmelig "internal secret" -u https://secrets.company.com
```
## Options
| Option | Description |
| ----------------------- | ------------------------------------------------------ |
| `-t, --title <title>` | Set a title for the secret |
| `-p, --password <pass>` | Protect with a password |
| `-e, --expires <time>` | Expiration: 5m, 30m, 1h, 4h, 12h, 1d, 3d, 7d, 14d, 28d |
| `-v, --views <number>` | Max views (1-9999, default: 1) |
| `-b, --burnable` | Burn after first view (default) |
| `--no-burnable` | Don't burn until all views used |
| `-u, --url <url>` | Base URL (default: https://hemmelig.app) |
| `-h, --help, /?` | Show help |
| `--version` | Show version |
## Security
- All encryption happens locally using AES-256-GCM
- Keys are derived using PBKDF2 with 600,000 iterations
- The decryption key is in the URL fragment (`#decryptionKey=...`), which is never sent to the server
- The server only stores encrypted data
## License
MIT

5
cli-go/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/HemmeligOrg/hemmelig-cli
go 1.24.0
require golang.org/x/crypto v0.45.0

2
cli-go/go.sum Normal file
View File

@@ -0,0 +1,2 @@
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=

343
cli-go/main.go Normal file
View File

@@ -0,0 +1,343 @@
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"golang.org/x/crypto/pbkdf2"
)
const version = "1.0.2"
var expirationTimes = map[string]int{
"5m": 300,
"30m": 1800,
"1h": 3600,
"4h": 14400,
"12h": 43200,
"1d": 86400,
"3d": 259200,
"7d": 604800,
"14d": 1209600,
"28d": 2419200,
}
type Options struct {
Secret string
Title string
Password string
Expires string
Views int
Burnable bool
BaseURL string
}
type SecretResponse struct {
ID string `json:"id"`
Error string `json:"error,omitempty"`
}
func generateKey() string {
b := make([]byte, 24)
rand.Read(b)
encoded := base64.URLEncoding.EncodeToString(b)
if len(encoded) > 32 {
return encoded[:32]
}
return encoded
}
func generateSalt() string {
return generateKey()
}
func deriveKey(password, salt string) []byte {
return pbkdf2.Key([]byte(password), []byte(salt), 1300000, 32, sha256.New)
}
func encrypt(data []byte, encryptionKey, salt string) ([]byte, error) {
key := deriveKey(encryptionKey, salt)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
iv := make([]byte, 12)
if _, err := rand.Read(iv); err != nil {
return nil, err
}
ciphertext := aesGCM.Seal(nil, iv, data, nil)
// Format: IV (12 bytes) + ciphertext (includes auth tag)
result := make([]byte, len(iv)+len(ciphertext))
copy(result, iv)
copy(result[len(iv):], ciphertext)
return result, nil
}
func uint8ArrayToObject(data []byte) map[string]int {
obj := make(map[string]int)
for i, b := range data {
obj[strconv.Itoa(i)] = int(b)
}
return obj
}
func createSecret(opts Options) (string, error) {
encryptionKey := opts.Password
if encryptionKey == "" {
encryptionKey = generateKey()
}
salt := generateSalt()
encryptedSecret, err := encrypt([]byte(opts.Secret), encryptionKey, salt)
if err != nil {
return "", fmt.Errorf("failed to encrypt secret: %w", err)
}
payload := map[string]interface{}{
"secret": uint8ArrayToObject(encryptedSecret),
"salt": salt,
"expiresAt": expirationTimes[opts.Expires],
"views": opts.Views,
"isBurnable": opts.Burnable,
}
if opts.Title != "" {
encryptedTitle, err := encrypt([]byte(opts.Title), encryptionKey, salt)
if err != nil {
return "", fmt.Errorf("failed to encrypt title: %w", err)
}
payload["title"] = uint8ArrayToObject(encryptedTitle)
}
if opts.Password != "" {
payload["password"] = opts.Password
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("failed to marshal payload: %w", err)
}
resp, err := http.Post(
opts.BaseURL+"/api/secrets",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
return "", fmt.Errorf("failed to create secret: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
var result SecretResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("failed to parse response: %w", err)
}
if resp.StatusCode != http.StatusCreated {
errMsg := result.Error
if errMsg == "" {
errMsg = "Unknown error"
}
return "", fmt.Errorf("failed to create secret: %s", errMsg)
}
var url string
if opts.Password != "" {
url = fmt.Sprintf("%s/secret/%s", opts.BaseURL, result.ID)
} else {
url = fmt.Sprintf("%s/secret/%s#decryptionKey=%s", opts.BaseURL, result.ID, encryptionKey)
}
return url, nil
}
func printHelp() {
fmt.Print(`
_ _ _ _
| | | | ___ _ __ ___ _ __ ___ ___| (_) __ _
| |_| |/ _ \ '_ ` + "`" + ` _ \| '_ ` + "`" + ` _ \ / _ \ | |/ _` + "`" + ` |
| _ | __/ | | | | | | | | | | __/ | | (_| |
|_| |_|\___|_| |_| |_|_| |_| |_|\___|_|_|\__, |
|___/
Create encrypted secrets from the command line
Usage:
hemmelig <secret> [options]
echo "secret" | hemmelig [options]
hemmelig --help
Options:
-t, --title <title> Set a title for the secret
-p, --password <pass> Protect with a password (if not set, key is in URL)
-e, --expires <time> Expiration time (default: 1d)
Valid: 5m, 30m, 1h, 4h, 12h, 1d, 3d, 7d, 14d, 28d
-v, --views <number> Max views before deletion (default: 1, max: 9999)
-b, --burnable Burn after first view (default: true)
--no-burnable Don't burn after first view
-u, --url <url> Base URL (default: https://hemmelig.app)
-h, --help, /? Show this help message
--version Show version number
Examples:
# Create a simple secret
hemmelig "my secret message"
# Create a secret with a title and 7-day expiration
hemmelig "my secret" -t "API Key" -e 7d
# Create a password-protected secret
hemmelig "my secret" -p "mypassword123"
# Create a secret with 5 views allowed
hemmelig "my secret" -v 5
# Pipe content from a file
cat ~/.ssh/id_rsa.pub | hemmelig -t "SSH Public Key"
# Use a self-hosted instance
hemmelig "my secret" -u https://secrets.mycompany.com
`)
}
func readStdin() string {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) != 0 {
return ""
}
data, err := io.ReadAll(os.Stdin)
if err != nil {
return ""
}
return strings.TrimRight(string(data), "\n\r")
}
func parseArgs(args []string) (Options, bool, bool) {
opts := Options{
Expires: "1d",
Views: 1,
Burnable: true,
BaseURL: "https://hemmelig.app",
}
showHelp := false
showVersion := false
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "-h", "--help", "/?":
showHelp = true
case "--version":
showVersion = true
case "-t", "--title":
if i+1 < len(args) {
i++
opts.Title = args[i]
}
case "-p", "--password":
if i+1 < len(args) {
i++
opts.Password = args[i]
}
case "-e", "--expires":
if i+1 < len(args) {
i++
opts.Expires = args[i]
}
case "-v", "--views":
if i+1 < len(args) {
i++
v, err := strconv.Atoi(args[i])
if err == nil {
opts.Views = v
}
}
case "-b", "--burnable":
opts.Burnable = true
case "--no-burnable":
opts.Burnable = false
case "-u", "--url":
if i+1 < len(args) {
i++
opts.BaseURL = args[i]
}
default:
if !strings.HasPrefix(arg, "-") && opts.Secret == "" {
opts.Secret = arg
}
}
}
return opts, showHelp, showVersion
}
func main() {
opts, showHelp, showVersion := parseArgs(os.Args[1:])
if showVersion {
fmt.Println(version)
os.Exit(0)
}
if showHelp {
printHelp()
os.Exit(0)
}
if opts.Secret == "" {
opts.Secret = readStdin()
}
if opts.Secret == "" {
fmt.Fprintln(os.Stderr, "Error: No secret provided. Use --help for usage information.")
os.Exit(1)
}
if _, ok := expirationTimes[opts.Expires]; !ok {
fmt.Fprintf(os.Stderr, "Error: Invalid expiration time \"%s\".\n", opts.Expires)
fmt.Fprintln(os.Stderr, "Valid options: 5m, 30m, 1h, 4h, 12h, 1d, 3d, 7d, 14d, 28d")
os.Exit(1)
}
if opts.Views < 1 || opts.Views > 9999 {
fmt.Fprintln(os.Stderr, "Error: Views must be between 1 and 9999.")
os.Exit(1)
}
url, err := createSecret(opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(url)
}