Compare commits

..

10 Commits

Author SHA1 Message Date
Rychliński Arkadiusz
ac14310642 refactor: Wszystkie pakiety przeniesione do internal 2022-11-27 22:39:04 +01:00
Rychliński Arkadiusz
b82a136fef feat: Wprowadzenie formatu age dla szyfrowania 2022-11-27 22:17:32 +01:00
Rychliński Arkadiusz
18c29e4621 feat: Poprawiona obsługa hasła z klawiatury 2022-11-23 14:52:22 +01:00
Rychliński Arkadiusz
473b76d1c9 Zaktualizowano README.md 2022-11-22 20:22:45 +01:00
Rychliński Arkadiusz
4dc83c3915 feat: Dodanie wyświetlania wersji 2022-11-22 20:22:45 +01:00
Rychliński Arkadiusz
8af918bdce feat: Dodano opis trybów encrypt/decrypt w helpie 2022-11-22 20:22:45 +01:00
Rychliński Arkadiusz
c8c1ba52b9 feat: Plik z hasłami musi być szyfrowany. Polecenie szyfrowania 2022-11-22 20:22:45 +01:00
Rychliński Arkadiusz
9ab0bcdb35 feat: Dodane funkcje kryptograficzne
Szyfrowanie i deszyfrowanie pliku AES-256 GCM.
2022-11-22 20:22:37 +01:00
Rychliński Arkadiusz
7d9c3d8f62 feat: Dodane parsowanie pliku pgpass
pgpass jest prsowany i przy uruchomieniu psql podawane jest
wybrane hasło pasujące do połaczenia
2022-11-22 20:02:52 +01:00
Rychliński Arkadiusz
c34954ef14 Plik licencji 2022-11-22 09:16:53 +01:00
13 changed files with 421 additions and 48 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
/.vscode
/output
/pgpass
/pgpass*
#exec
/multisql

7
LICENSE.txt Normal file
View File

@ -0,0 +1,7 @@
Copyright 2022 ark76
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -108,3 +108,53 @@ Powyższy select (dzieki \gset) zapamięta pozyskaną wartość (w tym wypadku p
Sekcja if / endif wykona się tylko wtedy, jeśli warunek był true i operacja `\o`, która tworzy nowy plik i resztę wyników kieruje do niego, też wykonuje się warunkowo.
W efekcie, jeśli warunek jest spełniony, w katalogu wynikowym działania skryptu pojawi się dodatkowy plik (ekstra.txt) co pozwoli łatwo wychwycić, dla których baz ów warunek występuje.
## Obsługa plików szyfrowanych
Od wersji 0.2.1 plik z hasłami (pgpass) musi być zaszyforwany. Szyfrowanie można wykonać następującym poleceniem:
```sh
multisql encrypt pgpass.sec pgpass
```
Program zapyta o nowe hasło a następnie zaszyfruje treść pliku pgpass i zapisze go do pliku pgpass.sec.
W konfiguracji należy ustawić plik wartość `"Passfile"` na `pgpass.sec`.
Zaszyfrowany plik można odszyfrować i zapisać do pliku jawnego lub podejrzeć:
```bash
multisql -P decrypt pgpass.sec # po udanym odszyfrowaniu wyświetli treść pliku
# lub
multisql -P decryp pgpass.sec newpgpass # w tym wariancie odszyfrowana treść zostanie zapisana do newpgpass
```
Opcja `-P` powoduje, że program pyta o hasło. Jeśli nie podano tej opcji, program
próbuje użyć hasła ustawionego w zmiennej środowiskowej `MULTISQLPASS`.
Zmienną można ustawić w następujący sposób:
Linux:
```bash
export MULTISQLPASS=abc
```
Windows cmd:
```cmd
set MULTISQLPASS=abc
```
Windows, powershell:
```powershell
$env:MULTISQLPASS = "abc"
```
Użycie opcji `-P` powoduje, że zmienna środowiskowa jest ignorowana i hasło pobierane jest z klawiatury.
> Uwaga: Od wersji 0.2.3 wykorzystywany jest format szyfrowania
> [age-encryption](https://age-encryption.org/).
> Plik haseł można więc szyfrować i deszyftować również narzędziem
> `age` (https://github.com/FiloSottile/age/releases/tag/v1.0.0)

View File

@ -1,10 +1,15 @@
package main
import (
"flag"
"fmt"
"os"
"runtime/debug"
"strings"
"baal.ar76.eu/x/pub/multisql/cfg"
"baal.ar76.eu/x/pub/multisql/mgr"
"baal.ar76.eu/x/pub/multisql/internal/cfg"
"baal.ar76.eu/x/pub/multisql/internal/mgr"
"baal.ar76.eu/x/pub/multisql/internal/pass"
"log"
)
@ -12,6 +17,15 @@ import (
func main() {
log.SetFlags(log.LstdFlags)
params := cfg.GetParams()
if params.Version {
printVersion()
return
}
args := flag.Args()
if len (args) > 0 {
encryption(args, params)
return
}
if params.LogFile != "" {
fh, err := os.OpenFile(params.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o664)
@ -44,7 +58,7 @@ func main() {
}
}
manager, err := mgr.Create(params.Verbose, params.OutDir, config)
manager, err := mgr.Create(params.Verbose, params.AskPass, params.OutDir, config)
if err != nil {
log.Fatalf("Nie można wystartować operacji: %v\n", err)
}
@ -56,3 +70,60 @@ func main() {
}
}
func encryption(args []string, params cfg.Parameters) {
switch strings.ToLower(args[0]) {
case "encrypt":
if len(args) < 3 {
log.Fatal("encrypt wymaga dwu plików: cel-plik.zaszyfrowany źródło-plik.jawny")
return
}
plain, err := os.ReadFile(args[2])
if err != nil {
log.Fatalf("Błąd odczytu pliku: %v", err)
}
password := pass.EnterMasterPass()
err = pass.EncryptFile(password, args[1], plain)
if err != nil {
_ = os.Remove(args[1]) // jeśli błąd to usuń plik
log.Fatalf("Błąd szyfrowania pliku: %v", err)
}
case "decrypt":
if len(args) < 2 {
log.Fatal("decryp wymaga podania pliku zaszyfrowanego")
return
}
password := pass.GetMasterPass(params.AskPass)
data, err := pass.DecryptFile(password, args[1])
if err !=nil {
log.Fatalf("Błąd deszyfrowania pliku: %v", err)
}
if len(args) > 2 {
err = os.WriteFile(args[2], data, 0o600)
if err != nil {
log.Fatalf("Błąd zapisu pliku: %v", err)
}
log.Printf("Zapisano odszyfrowane dane do pliku %s", args[2])
} else {
os.Stdout.Write(data)
}
default:
log.Println("W trybie komend wymagane jest podanie polecenie encrypt lub decrypt i nazw plików: zaszyfrowanego i jawnego");
log.Println("Np:")
log.Println("multisql encrypt hasła.zaszyfrowany hasła")
log.Println("multisql decrypt hasła.zaszyfrowany hasła")
}
}
func printVersion() {
bi, ok := debug.ReadBuildInfo()
if !ok {
log.Fatalf("Błąd odczytu informacji o wersji")
}
fmt.Fprintf(os.Stderr, "Wersja głównego modułu: %v\n", bi.Main.Version)
fmt.Fprintln(os.Stderr, bi)
}

10
go.mod
View File

@ -1,3 +1,13 @@
module baal.ar76.eu/x/pub/multisql
go 1.19
require (
filippo.io/age v1.0.0
golang.org/x/term v0.2.0
)
require (
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/sys v0.2.0 // indirect
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc=
filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=

View File

@ -8,13 +8,17 @@ import (
"os"
)
const MULTISQLPASS = "MULTISQLPASS" // nazwa ziennej środowiskowej z hasłem
type Parameters struct {
Verbose bool
ConfigFile string
Passfile string
OutDir string
SqlDir string
LogFile string
LogFile string
Verbose bool
AskPass bool
Version bool
}
var params Parameters
@ -26,6 +30,9 @@ func init() {
flag.StringVar(&params.OutDir, "outdir", "", "Katalog (istniejący i z prawem do zapisu), do którego generowane są wyniki")
flag.StringVar(&params.SqlDir, "sqldir", "scripts", "Katalog, w którym znajdują się skrypty do uruchomienia")
flag.StringVar(&params.LogFile, "log", "", "Plik, do którego zostanie dopisany log programu")
flag.BoolVar(&params.AskPass, "P", false, "Pytaj o hasło. Jeśli nie podane wymaga się hasła w ")
flag.BoolVar(&params.Version, "version", false, "Wypisuje wersję i kończy działanie")
flag.Usage = printUsage
flag.Parse()
}
@ -58,6 +65,11 @@ output:
Użycie:
multisql -outdir /tmp -sqldir /data/skrypty -passfile ./hasla
Użycie w trybie szyfrowania:
multisql encrypt plik.wynikowy.zaszyfrowany plik.zródłowy
lub
multisql [-P] decrypt plik.źródłowy.zaszyfrowany [plik.wynikowy.jawny]
Opis flag:
`,
)
@ -96,7 +108,7 @@ Plik multisql.conf ma format JSON, np:
_ = json.Indent(&out, b, "> ", "\t")
_, _ = out.WriteTo(os.Stderr)
fmt.Fprintln(os.Stderr, `
fmt.Fprintf(os.Stderr, `
PsqlExec jest opcjonalny - jesli nie zostanie podany wyszukuje się programu psql.exe w PATH.
@ -110,5 +122,16 @@ Format pliku passfile jest następujący:
hostname:port:database:username:password
W szczególach opisano go na https://www.postgresql.org/docs/current/libpq-pgpass.html.`)
W szczegółach opisano go na https://www.postgresql.org/docs/current/libpq-pgpass.html.
Aktualnie obsługiwana jest jedynie zaszyforowana postać pliku. Przed użyciem należy
plik zaszyfrować:
multsql encrypt pgpass.encrypted pgpass
Przy użyciu (odszyfrowaniu) pliku, hasło jest pobierane ze zmiennej
środowiskowej %s lub z klawiatury, jeśli użyto opcji -P.
`, MULTISQLPASS)
}

View File

@ -8,23 +8,26 @@ import (
"sync"
"time"
"baal.ar76.eu/x/pub/multisql/cfg"
"baal.ar76.eu/x/pub/multisql/psql"
"baal.ar76.eu/x/pub/multisql/internal/cfg"
"baal.ar76.eu/x/pub/multisql/internal/pass"
"baal.ar76.eu/x/pub/multisql/internal/psql"
)
type Manager struct {
config cfg.Config
runDir string
config cfg.Config
runDir string
verbose bool
askPass bool
}
func Create(verbose bool, outdir string, config cfg.Config) (Manager, error) {
func Create(verbose bool, askPass bool, outdir string, config cfg.Config) (Manager, error) {
runDir := createRunDir(outdir)
manager := Manager{
config: config,
runDir: runDir,
config: config,
runDir: runDir,
verbose: verbose,
askPass: askPass,
}
return manager, nil
@ -69,6 +72,16 @@ func (self Manager) Run() error {
return err
}
var passdb *pass.PassDb
if self.config.PassFile != "" {
password := pass.GetMasterPass(self.askPass)
passdb, err = pass.Load(self.config.PassFile, password)
if err != nil {
return err
}
}
var group sync.WaitGroup
for _, con := range self.config.Connections {
@ -84,7 +97,7 @@ func (self Manager) Run() error {
go self.Logger(&group, stream, con)
sql := psql.Create(
self.config.PassFile,
passdb,
dbDir,
scripts,
con,
@ -105,7 +118,6 @@ func (self Manager) createDirPerCon(con cfg.Connection) (string, error) {
return dir, err
}
func (self Manager) Logger(group *sync.WaitGroup, stream <-chan psql.Result, con cfg.Connection) {
for event := range stream {
if event.Err != nil {

65
internal/pass/crypt.go Normal file
View File

@ -0,0 +1,65 @@
package pass
import (
"bytes"
"fmt"
"io"
"os"
"filippo.io/age"
"filippo.io/age/armor"
)
func DecryptFile(password []byte, file string) ([]byte, error) {
identity, err := age.NewScryptIdentity(string(password))
if err != nil {
return nil, fmt.Errorf("błąd przetwarzania hasła: %w", err)
}
fd, err := os.Open(file)
if err != nil {
return nil, fmt.Errorf("błąd otwarcia pliku: %w", err)
}
defer fd.Close()
in := armor.NewReader(fd)
r, err := age.Decrypt(in, identity)
if err != nil {
return nil, fmt.Errorf("błąd deszyfrowania #01: %w", err)
}
out := &bytes.Buffer{}
if _, err := io.Copy(out, r); err != nil {
return nil, fmt.Errorf("błąd deszyfrowania #02: %v", err)
}
return out.Bytes(), nil
}
func EncryptFile(password []byte, file string, data []byte) error {
recipient, err := age.NewScryptRecipient(string(password))
if err != nil {
return fmt.Errorf("błąd przetwarzania hasła: %w", err)
}
fd, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0o600)
if err != nil {
return fmt.Errorf("błąd tworzenia pliku: %w", err)
}
defer fd.Close()
out := armor.NewWriter(fd) // armor
defer out.Close()
w, err := age.Encrypt(out, recipient)
if err != nil {
return fmt.Errorf("błąd szyfrowania #01: %w", err)
}
_, err = w.Write(data)
if err != nil {
return fmt.Errorf("błąd szyfrowania #02: %w", err)
}
err = w.Close()
if err != nil {
return fmt.Errorf("błąd szyfrowania #03: %w", err)
}
return nil
}

80
internal/pass/pass.go Normal file
View File

@ -0,0 +1,80 @@
package pass
import (
"bytes"
"fmt"
)
type pgpassrow struct {
hostname string
port string
database string
username string
password string
}
type PassDb struct {
db []pgpassrow
cache map[string]*pgpassrow
}
func Load(file string, master []byte) (*PassDb, error) {
data, err := DecryptFile(master, file)
if err != nil {
return nil, err
}
lines := bytes.Split(data, []byte{'\n'})
var rows []pgpassrow
for i, line := range lines {
line = bytes.TrimRight(line, "\n\r \t")
if len(line) == 0 || line[0] == byte('#') {
continue
}
fields := bytes.Split(line, []byte{':'})
if len(fields) != 5 {
return nil, fmt.Errorf("błąd w pliku hasłeł %s:%d: niewłaściwa liczba pól", file, i)
}
rows = append(rows, pgpassrow{
hostname: string(fields[0]),
port: string(fields[1]),
database: string(fields[2]),
username: string(fields[3]),
password: string(fields[4]),
})
}
return &PassDb{
db: rows,
cache: make(map[string]*pgpassrow),
}, nil
}
func match(value string, pattern string) bool {
return pattern == "*" || pattern == value
}
func (self *PassDb) FindPassword(host string, port uint, database string, user string) string {
myport := fmt.Sprintf("%d", port)
key := fmt.Sprintf("%s:%s:%s:%s", host, myport, database, user)
cached, ok := self.cache[key]
if ok {
return cached.password
}
// pierwszy pasujący do wzorca
for i := range self.db {
if match(host, self.db[i].hostname) &&
match(myport, self.db[i].port) &&
match(database, self.db[i].database) &&
match(user, self.db[i].username) {
self.cache[key] = &self.db[i]
return self.db[i].password
}
}
return ""
}

55
internal/pass/term.go Normal file
View File

@ -0,0 +1,55 @@
package pass
import (
"bytes"
"fmt"
"log"
"os"
"baal.ar76.eu/x/pub/multisql/internal/cfg"
"golang.org/x/term"
)
func GetMasterPass(askPass bool) []byte {
if askPass {
res, err := termGetPassword("Podaj hasło")
if err != nil {
log.Fatalln("Nie udało się wczytać hasła z konsoli")
}
return res
}
password := os.Getenv(cfg.MULTISQLPASS)
if password != "" {
return []byte(password)
}
log.Fatalf("Nieustalone hasło. Użyj flagi -P lub ustaw hasło w zmiennej %s", cfg.MULTISQLPASS)
return nil
}
func EnterMasterPass() []byte {
p1, err := termGetPassword("Podaj nowe hasło (min 8 znaków)")
if err != nil {
log.Fatalf("Błąd wczytywania hasła: %v", err)
}
if len(p1) < 8 {
log.Fatalf("podano zbyt krótkie hasło")
}
p2, err := termGetPassword("Powtórz nowe hasło")
if err != nil {
log.Fatalf("Błąd wczytywania hasła: %v", err)
}
if !bytes.Equal(p1, p2) {
log.Fatalf("podane hasła są różne")
}
return p1
}
func termGetPassword(prompt string) ([]byte, error) {
fmt.Fprintf(os.Stderr, "%s: ", prompt)
passwd, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(os.Stderr)
return passwd, err
}

View File

@ -9,16 +9,17 @@ import (
"path/filepath"
"runtime"
"baal.ar76.eu/x/pub/multisql/cfg"
"baal.ar76.eu/x/pub/multisql/internal/cfg"
"baal.ar76.eu/x/pub/multisql/internal/pass"
)
type Instance struct {
outdir string
scripts []string
passfile string
psqlExe string
conn cfg.Connection
verbose bool
outdir string
scripts []string
pasdb *pass.PassDb
psqlExe string
conn cfg.Connection
verbose bool
}
type Result struct {
@ -26,32 +27,22 @@ type Result struct {
Err error
}
func Create(passfile string, outdir string, scripts []string, conn cfg.Connection, psqlExe string, verbose bool) (instnace Instance) {
func Create(passdb *pass.PassDb, outdir string, scripts []string, conn cfg.Connection, psqlExe string, verbose bool) (instnace Instance) {
instnace = Instance{
outdir: outdir,
scripts: scripts,
passfile: passfile,
conn: conn,
psqlExe: psqlExe,
verbose: verbose,
outdir: outdir,
scripts: scripts,
pasdb: passdb,
conn: conn,
psqlExe: psqlExe,
verbose: verbose,
}
return instnace
}
func (self Instance) Exec(result chan<- Result) {
absPassFile := ""
if self.passfile != "" {
var err error
absPassFile, err = filepath.Abs(self.passfile)
if err != nil {
result <- Result{
Script: "*",
Err: fmt.Errorf("problem ze zbudowaniem ściężki do pliku haseł: %w", err),
}
return
}
}
connString := fmt.Sprintf("host=%s port=%d dbname=%s user=%s", self.conn.Host, self.conn.Port, self.conn.DbName, self.conn.User)
for _, scr := range self.scripts {
@ -65,7 +56,8 @@ func (self Instance) Exec(result chan<- Result) {
}
continue
}
err = self.cmdPsql(outdir, connString, scr, absPassFile)
password := self.pasdb.FindPassword(self.conn.Host, self.conn.Port, self.conn.DbName, self.conn.User)
err = self.cmdPsql(outdir, connString, scr, password)
result <- Result{
Script: scr,
Err: err,
@ -74,7 +66,7 @@ func (self Instance) Exec(result chan<- Result) {
close(result)
}
func (self Instance) cmdPsql(outdir string, connString string, script string, passfile string) error {
func (self Instance) cmdPsql(outdir string, connString string, script string, password string) error {
absScript, err := filepath.Abs(script)
if err != nil {
return fmt.Errorf("problem ze zbudowaniem ściezki do skryptu: %w", err)
@ -112,13 +104,12 @@ func (self Instance) cmdPsql(outdir string, connString string, script string, pa
cmd.Stdout = fhOut
cmd.Dir = outdir
if passfile != "" {
if password != "" {
env := os.Environ()
env = append(env, "PGPASSFILE=" + passfile)
env = append(env, "PGPASSWORD="+password)
cmd.Env = env
}
if self.verbose {
log.Printf("Uruchamiam %s", cmd.String())
}