mirror of
https://codeberg.org/Kankys/doxa.git
synced 2026-11-11 14:44:36 +01:00
new-doxa-monorepo
This commit is contained in:
1 parent
b4401527a3
commit
5eacbb0182
162 files changed
+21820
-2
No files matched your search
@@ -0,0 +1,317 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user