Replace Python launcher with native Go UI
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func openssl_path() (string, error) {
|
||||
path, err := exec.LookPath("openssl")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("required command not found: openssl")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func openssl_subject_name(commonName string) (string, error) {
|
||||
if strings.ContainsAny(commonName, "\x00\r\n") {
|
||||
return "", fmt.Errorf("certificate name cannot contain control characters")
|
||||
}
|
||||
escaped := strings.NewReplacer(`\`, `\\`, `/`, `\/`).Replace(commonName)
|
||||
return "/CN=" + escaped, nil
|
||||
}
|
||||
|
||||
func install_private_bytes(path string, contents []byte) error {
|
||||
return write_private_file(path, func(writer io.Writer) error {
|
||||
_, err := writer.Write(contents)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) generate_certificate() error {
|
||||
openssl, err := openssl_path()
|
||||
if err != nil {
|
||||
return app.ui.message(err.Error())
|
||||
}
|
||||
if _, err := os.Stat(app.paths.CertFile); err == nil {
|
||||
replace, err := app.ui.confirm("A certificate already exists. Replace it? This may affect your registered identity on servers.")
|
||||
if err != nil || !replace {
|
||||
return err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return app.ui.message("Could not inspect the certificate: " + err.Error())
|
||||
}
|
||||
|
||||
commonName, cancelled, err := app.ui.input("Enter a name for your certificate, such as your username:", "barnard", false)
|
||||
if err != nil || cancelled {
|
||||
return err
|
||||
}
|
||||
commonName = strings.TrimSpace(commonName)
|
||||
if commonName == "" {
|
||||
commonName = "barnard"
|
||||
}
|
||||
subject, err := openssl_subject_name(commonName)
|
||||
if err != nil {
|
||||
return app.ui.message(err.Error())
|
||||
}
|
||||
|
||||
keyFile, err := os.CreateTemp(app.paths.ConfigDir, ".certificate-key-")
|
||||
if err != nil {
|
||||
return app.ui.message("Failed to create a certificate: " + err.Error())
|
||||
}
|
||||
keyPath := keyFile.Name()
|
||||
keyFile.Close()
|
||||
defer os.Remove(keyPath)
|
||||
certificateFile, err := os.CreateTemp(app.paths.ConfigDir, ".certificate-public-")
|
||||
if err != nil {
|
||||
return app.ui.message("Failed to create a certificate: " + err.Error())
|
||||
}
|
||||
certificatePath := certificateFile.Name()
|
||||
certificateFile.Close()
|
||||
defer os.Remove(certificatePath)
|
||||
|
||||
command := exec.Command(openssl, "req", "-x509", "-newkey", "rsa:2048", "-keyout", keyPath, "-out", certificatePath, "-days", "3650", "-nodes", "-subj", subject)
|
||||
if output, err := command.CombinedOutput(); err != nil {
|
||||
app.log_line("OpenSSL certificate generation failed: " + strings.TrimSpace(string(output)))
|
||||
return app.ui.message("Failed to generate certificate.")
|
||||
}
|
||||
privateKey, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return app.ui.message("Failed to read generated private key: " + err.Error())
|
||||
}
|
||||
certificate, err := os.ReadFile(certificatePath)
|
||||
if err != nil {
|
||||
return app.ui.message("Failed to read generated certificate: " + err.Error())
|
||||
}
|
||||
combined := append(append(append([]byte(nil), privateKey...), '\n'), certificate...)
|
||||
if err := install_private_bytes(app.paths.CertFile, combined); err != nil {
|
||||
return app.ui.message("Failed to install generated certificate: " + err.Error())
|
||||
}
|
||||
app.log_line("Generated certificate " + app.paths.CertFile)
|
||||
return app.ui.message("Certificate generated successfully.")
|
||||
}
|
||||
|
||||
func (app *App) view_certificate() error {
|
||||
openssl, err := openssl_path()
|
||||
if err != nil {
|
||||
return app.ui.message(err.Error())
|
||||
}
|
||||
if _, err := os.Stat(app.paths.CertFile); os.IsNotExist(err) {
|
||||
return app.ui.message("No certificate found: " + app.paths.CertFile)
|
||||
} else if err != nil {
|
||||
return app.ui.message("Could not inspect the certificate: " + err.Error())
|
||||
}
|
||||
command := exec.Command(openssl, "x509", "-in", app.paths.CertFile, "-noout", "-subject", "-dates", "-fingerprint")
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil || strings.TrimSpace(string(output)) == "" {
|
||||
return app.ui.message("Could not read certificate information.")
|
||||
}
|
||||
return app.ui.message(strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
func validate_certificate_pair(openssl, path string) error {
|
||||
certificateCheck := exec.Command(openssl, "x509", "-in", path, "-noout")
|
||||
if err := certificateCheck.Run(); err != nil {
|
||||
return fmt.Errorf("the file does not contain a valid PEM certificate")
|
||||
}
|
||||
keyCheck := exec.Command(openssl, "pkey", "-in", path, "-check", "-noout")
|
||||
if err := keyCheck.Run(); err != nil {
|
||||
return fmt.Errorf("the file does not contain a valid private key")
|
||||
}
|
||||
certificatePublic, err := exec.Command(openssl, "x509", "-in", path, "-pubkey", "-noout").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read the certificate public key")
|
||||
}
|
||||
keyPublic, err := exec.Command(openssl, "pkey", "-in", path, "-pubout").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read the private key public key")
|
||||
}
|
||||
if !bytes.Equal(bytes.TrimSpace(certificatePublic), bytes.TrimSpace(keyPublic)) {
|
||||
return fmt.Errorf("the certificate and private key do not match")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *App) import_certificate() error {
|
||||
openssl, err := openssl_path()
|
||||
if err != nil {
|
||||
return app.ui.message(err.Error())
|
||||
}
|
||||
rawPath, cancelled, err := app.ui.input("Enter the full path to a PEM file containing both the certificate and private key:", "", false)
|
||||
if err != nil || cancelled {
|
||||
return err
|
||||
}
|
||||
rawPath = strings.TrimSpace(rawPath)
|
||||
if rawPath == "" {
|
||||
return nil
|
||||
}
|
||||
path, err := expand_user_path(rawPath)
|
||||
if err != nil {
|
||||
return app.ui.message("Could not resolve certificate path: " + err.Error())
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return app.ui.message("File not found: " + path)
|
||||
}
|
||||
if err != nil {
|
||||
return app.ui.message("Could not inspect certificate file: " + err.Error())
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return app.ui.message("Certificate path is not a regular file: " + path)
|
||||
}
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return app.ui.message("Failed to read certificate: " + err.Error())
|
||||
}
|
||||
validationFile, err := os.CreateTemp(app.paths.ConfigDir, ".certificate-import-")
|
||||
if err != nil {
|
||||
return app.ui.message("Failed to prepare certificate validation: " + err.Error())
|
||||
}
|
||||
validationPath := validationFile.Name()
|
||||
defer os.Remove(validationPath)
|
||||
if err := validationFile.Chmod(0600); err != nil {
|
||||
validationFile.Close()
|
||||
return app.ui.message("Failed to protect certificate validation file: " + err.Error())
|
||||
}
|
||||
if _, err := validationFile.Write(contents); err != nil {
|
||||
validationFile.Close()
|
||||
return app.ui.message("Failed to prepare certificate validation: " + err.Error())
|
||||
}
|
||||
if err := validationFile.Close(); err != nil {
|
||||
return app.ui.message("Failed to prepare certificate validation: " + err.Error())
|
||||
}
|
||||
if err := validate_certificate_pair(openssl, validationPath); err != nil {
|
||||
return app.ui.message(err.Error() + ".")
|
||||
}
|
||||
if _, err := os.Stat(app.paths.CertFile); err == nil {
|
||||
samePath := false
|
||||
sourcePath, sourceErr := filepath.EvalSymlinks(path)
|
||||
destinationPath, destinationErr := filepath.EvalSymlinks(app.paths.CertFile)
|
||||
if sourceErr == nil && destinationErr == nil {
|
||||
samePath = sourcePath == destinationPath
|
||||
}
|
||||
if samePath {
|
||||
return app.ui.message("That certificate is already the active Barnard certificate.")
|
||||
}
|
||||
replace, err := app.ui.confirm("A certificate already exists. Replace it?")
|
||||
if err != nil || !replace {
|
||||
return err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return app.ui.message("Could not inspect the existing certificate: " + err.Error())
|
||||
}
|
||||
if err := install_private_bytes(app.paths.CertFile, contents); err != nil {
|
||||
return app.ui.message("Failed to import certificate: " + err.Error())
|
||||
}
|
||||
app.log_line("Imported certificate " + app.paths.CertFile)
|
||||
return app.ui.message("Certificate imported successfully.")
|
||||
}
|
||||
|
||||
func (app *App) manage_certificate() error {
|
||||
options := []string{"Generate", "View", "Import", "Go Back"}
|
||||
for {
|
||||
selection, cancelled, err := app.ui.menu(options)
|
||||
if err != nil || cancelled || selection == len(options)-1 {
|
||||
return err
|
||||
}
|
||||
switch options[selection] {
|
||||
case "Generate":
|
||||
err = app.generate_certificate()
|
||||
case "View":
|
||||
err = app.view_certificate()
|
||||
case "Import":
|
||||
err = app.import_certificate()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user