90 lines
3.0 KiB
Go
90 lines
3.0 KiB
Go
package keymgr
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"ssh-access-tui/internal/model"
|
|
)
|
|
|
|
// BuildResult describes one user's finished delivery archive.
|
|
type BuildResult struct {
|
|
Username string
|
|
ArchivePath string
|
|
Fingerprint string
|
|
PublicKeyLine string // capture before the scratch dir (and its .pub file) is wiped
|
|
}
|
|
|
|
// BuildUserArchive generates a fresh keypair for username, writes the
|
|
// client config and Windows instructions alongside it in a scratch
|
|
// directory, packs everything into a single AES-256-encrypted 7z archive
|
|
// under outDir, and then wipes the scratch directory (including the
|
|
// plaintext private key) from disk. password is shared across one export
|
|
// batch and must be communicated to the recipient out-of-band.
|
|
func BuildUserArchive(scratchDir, outDir, jumpHost string, jumpPort int, username, password string, hosts []model.Host) (*BuildResult, error) {
|
|
userScratch := filepath.Join(scratchDir, username)
|
|
if err := os.MkdirAll(userScratch, 0o700); err != nil {
|
|
return nil, fmt.Errorf("create scratch dir: %w", err)
|
|
}
|
|
defer Wipe(userScratch)
|
|
|
|
kp, err := GeneratePair(userScratch, username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate keypair: %w", err)
|
|
}
|
|
keyFileName := filepath.Base(kp.PrivateKeyPath)
|
|
|
|
cfg := RenderClientConfig(jumpHost, jumpPort, username, keyFileName, hosts)
|
|
if err := os.WriteFile(filepath.Join(userScratch, "config"), []byte(cfg), 0o600); err != nil {
|
|
return nil, fmt.Errorf("write config: %w", err)
|
|
}
|
|
|
|
instr := RenderInstructions(username, jumpHost, keyFileName, hosts)
|
|
if err := os.WriteFile(filepath.Join(userScratch, "INSTRUCTIONS.txt"), []byte(instr), 0o600); err != nil {
|
|
return nil, fmt.Errorf("write instructions: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(outDir, 0o750); err != nil {
|
|
return nil, fmt.Errorf("create output dir: %w", err)
|
|
}
|
|
archivePath := filepath.Join(outDir, username+"_ssh_access.7z")
|
|
_ = os.Remove(archivePath) // 7z refuses to overwrite silently-appends; start clean
|
|
|
|
cmd := exec.Command("7z", "a",
|
|
"-p"+password,
|
|
"-mhe=on", // encrypt file names/headers too, not just contents
|
|
"-mx=9",
|
|
archivePath,
|
|
filepath.Join(userScratch, keyFileName),
|
|
filepath.Join(userScratch, keyFileName+".pub"),
|
|
filepath.Join(userScratch, "config"),
|
|
filepath.Join(userScratch, "INSTRUCTIONS.txt"),
|
|
)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return nil, fmt.Errorf("7z: %w: %s", err, out)
|
|
}
|
|
|
|
return &BuildResult{
|
|
Username: username,
|
|
ArchivePath: archivePath,
|
|
Fingerprint: kp.Fingerprint,
|
|
PublicKeyLine: kp.PublicKeyLine,
|
|
}, nil
|
|
}
|
|
|
|
// Wipe securely removes a scratch directory (best-effort shred of file
|
|
// contents, then unlink) so the private key does not linger in free disk
|
|
// space or in any backup that might sweep the tool's working directory.
|
|
func Wipe(dir string) {
|
|
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return nil
|
|
}
|
|
_ = exec.Command("shred", "-u", "-n", "1", path).Run()
|
|
return nil
|
|
})
|
|
_ = os.RemoveAll(dir)
|
|
}
|