mirror of
https://codeberg.org/Kankys/doxa.git
synced 2026-11-11 14:44:36 +01:00
318 lines
7.9 KiB
Go
318 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
type CipherConfig struct {
|
|
Version string `json:"version"`
|
|
Mappings map[string]string `json:"mappings"`
|
|
}
|
|
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
// getCipherDir returns the config directory ~/.doxa-secret
|
|
func getCipherDir() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
configDir := filepath.Join(home, ".doxa-secret")
|
|
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
|
err = os.MkdirAll(configDir, 0755)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return configDir, nil
|
|
}
|
|
|
|
// getExportDir returns the export directory ~/doxa-secret/export
|
|
func getExportDir() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dir := filepath.Join(home, "doxa-secret", "export")
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
err = os.MkdirAll(dir, 0755)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return dir, nil
|
|
}
|
|
|
|
// getImportDir returns the import directory ~/doxa-secret/import
|
|
func getImportDir() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dir := filepath.Join(home, "doxa-secret", "import")
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
err = os.MkdirAll(dir, 0755)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return dir, nil
|
|
}
|
|
|
|
// generateRandomCode generates a unique 5-character alphanumeric string
|
|
func generateRandomCode(existing map[string]bool) (string, error) {
|
|
for {
|
|
code := make([]byte, 5)
|
|
for i := 0; i < 5; i++ {
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
code[i] = charset[n.Int64()]
|
|
}
|
|
codeStr := string(code)
|
|
if !existing[codeStr] {
|
|
existing[codeStr] = true
|
|
return codeStr, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// GenerateNewCipher creates a new cipher configuration with 5-character mappings
|
|
func GenerateNewCipher(name string) error {
|
|
dir, err := getCipherDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
destPath := filepath.Join(dir, name+".json")
|
|
if _, err := os.Stat(destPath); err == nil {
|
|
return fmt.Errorf("šifra s názvem '%s' již existuje", name)
|
|
}
|
|
|
|
// Basic characters to encrypt
|
|
charsToMap := []string{
|
|
// Space, comma, dot
|
|
" ", ",", ".",
|
|
// Digits
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
// Lowercase letters
|
|
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
|
// Uppercase letters
|
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
|
// Punctuation and special characters
|
|
"!", "?", "-", "+", "=", "*", "/", "\\", "_", "@", "#", "$", "%", "^", "&", "(", ")", "[", "]", "{", "}", ":", ";", "\"", "'", "<", ">", "|", "~", "`",
|
|
// Control chars (newlines, tabs)
|
|
"\n", "\r", "\t",
|
|
}
|
|
|
|
mappings := make(map[string]string)
|
|
usedCodes := make(map[string]bool)
|
|
|
|
for _, ch := range charsToMap {
|
|
code, err := generateRandomCode(usedCodes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mappings[ch] = code
|
|
}
|
|
|
|
config := CipherConfig{
|
|
Version: "1.0",
|
|
Mappings: mappings,
|
|
}
|
|
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(destPath, data, 0644)
|
|
}
|
|
|
|
// LoadCipher loads a cipher by its file name (without path) from the configuration directory
|
|
func LoadCipher(name string) (*CipherConfig, error) {
|
|
dir, err := getCipherDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path := filepath.Join(dir, name+".json")
|
|
return LoadCipherFromPath(path)
|
|
}
|
|
|
|
// LoadCipherFromPath loads a cipher from an absolute path
|
|
func LoadCipherFromPath(path string) (*CipherConfig, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var config CipherConfig
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate cipher: ensure each code has length 5 and is unique
|
|
if config.Mappings == nil {
|
|
return nil, errors.New("abeceda neobsahuje žádné mapování")
|
|
}
|
|
|
|
used := make(map[string]bool)
|
|
for char, code := range config.Mappings {
|
|
if len(code) != 5 {
|
|
return nil, fmt.Errorf("neplatná délka šifry pro znak '%s': očekáváno 5 znaků, nalezeno %d", char, len(code))
|
|
}
|
|
if used[code] {
|
|
return nil, fmt.Errorf("duplicitní šifra '%s' v mapování", code)
|
|
}
|
|
used[code] = true
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// ListCiphers returns a list of all cipher names (without .json extension) in the configuration directory
|
|
func ListCiphers() ([]string, error) {
|
|
dir, err := getCipherDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var names []string
|
|
for _, f := range files {
|
|
if !f.IsDir() && filepath.Ext(f.Name()) == ".json" {
|
|
name := f.Name()
|
|
names = append(names, name[:len(name)-len(".json")])
|
|
}
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// RemoveDiacritics converts Czech/accented runes to their basic Latin counterparts
|
|
func RemoveDiacritics(text string) string {
|
|
replacements := map[rune]rune{
|
|
'á': 'a', 'č': 'c', 'ď': 'd', 'é': 'e', 'ě': 'e', 'í': 'i', 'ň': 'n', 'ó': 'o', 'ř': 'r', 'š': 's', 'ť': 't', 'ú': 'u', 'ů': 'u', 'ý': 'y', 'ž': 'z',
|
|
'Á': 'A', 'Č': 'C', 'Ď': 'D', 'É': 'E', 'Ě': 'E', 'Í': 'I', 'Ň': 'N', 'Ó': 'O', 'Ř': 'R', 'Š': 'S', 'Ť': 'T', 'Ú': 'U', 'Ů': 'U', 'Ý': 'Y', 'Ž': 'Z',
|
|
}
|
|
runes := []rune(text)
|
|
for i, r := range runes {
|
|
if repl, ok := replacements[r]; ok {
|
|
runes[i] = repl
|
|
}
|
|
}
|
|
return string(runes)
|
|
}
|
|
|
|
// EncryptMessage encrypts plain text into secret code
|
|
func EncryptMessage(plainText string, config *CipherConfig) string {
|
|
// Normalize text to Latin alphabet without diacritics
|
|
plainText = RemoveDiacritics(plainText)
|
|
|
|
var encrypted string
|
|
// Iterate over the string as runes to correctly handle UTF-8
|
|
for _, r := range plainText {
|
|
charStr := string(r)
|
|
if code, ok := config.Mappings[charStr]; ok {
|
|
encrypted += code
|
|
} else {
|
|
// If not mapped, keep the character as is
|
|
encrypted += charStr
|
|
}
|
|
}
|
|
return encrypted
|
|
}
|
|
|
|
// DecryptMessage decrypts secret code back into plain text
|
|
func DecryptMessage(encryptedText string, config *CipherConfig) string {
|
|
// Create reverse mapping
|
|
reverse := make(map[string]string)
|
|
for k, v := range config.Mappings {
|
|
reverse[v] = k
|
|
}
|
|
|
|
var decrypted string
|
|
runes := []rune(encryptedText)
|
|
n := len(runes)
|
|
|
|
for i := 0; i < n; {
|
|
// Check if we can extract a 5-character block
|
|
if i+5 <= n {
|
|
block := string(runes[i : i+5])
|
|
if char, ok := reverse[block]; ok {
|
|
decrypted += char
|
|
i += 5
|
|
continue
|
|
}
|
|
}
|
|
// Fallback: if it's not a valid 5-char code block, or not found, consume 1 rune
|
|
decrypted += string(runes[i])
|
|
i++
|
|
}
|
|
|
|
return decrypted
|
|
}
|
|
|
|
// ImportCipher copies a cipher JSON file from any location to ~/.doxa-secret/
|
|
func ImportCipher(srcPath string) (string, error) {
|
|
config, err := LoadCipherFromPath(srcPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("chyba při načtení/validaci šifry: %v", err)
|
|
}
|
|
|
|
dir, err := getCipherDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
baseName := filepath.Base(srcPath)
|
|
nameWithoutExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
|
destPath := filepath.Join(dir, baseName)
|
|
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
err = os.WriteFile(destPath, data, 0644)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return nameWithoutExt, nil
|
|
}
|
|
|
|
// ExportCipher copies the active cipher JSON to a target path
|
|
func ExportCipher(name string, destPath string) error {
|
|
dir, err := getCipherDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
srcPath := filepath.Join(dir, name+".json")
|
|
data, err := os.ReadFile(srcPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure destination directory exists
|
|
destDir := filepath.Dir(destPath)
|
|
if _, err := os.Stat(destDir); os.IsNotExist(err) {
|
|
err = os.MkdirAll(destDir, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return os.WriteFile(destPath, data, 0644)
|
|
}
|