Loading batch/batch.go +2 −2 Original line number Diff line number Diff line Loading @@ -217,13 +217,13 @@ Match User %s X11Forwarding no AllowTcpForwarding no PermitTunnel no ForceCommand %s --ssh ForceCommand /usr/sbin/nologin AuthorizedKeysCommand %s authorized-keys %%u %%t %%k AuthorizedKeysCommandUser %s --- Then run: sudo systemctl reload sshd --- `, os.Getenv("USER"), bulletin, bulletin, os.Getenv("USER")) `, os.Getenv("USER"), bulletin, os.Getenv("USER")) _, _ = ask.GetLine("Press Enter to continue after configuring sshd...") } Loading key/key.go +1 −1 Original line number Diff line number Diff line Loading @@ -20,7 +20,7 @@ import ( "golang.org/x/sys/unix" ) var keytemplate = `command="%s -u %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s` var keytemplate = `command="%s --user %s",restrict %s` // Add adds an ssh key to the `authorized_keys` file. func Add(login, public string) error { Loading onboard/onboard.go +22 −19 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import ( "fmt" "os" "strings" "time" "git.lyda.ie/pp/bulletin/ask" "git.lyda.ie/pp/bulletin/authorized" Loading Loading @@ -48,13 +49,16 @@ func Run(fingerprint, pubkeyStr string) int { if len(shibboleths) > 0 { fmt.Println("Before proceeding, please answer the following questions:") fmt.Println() for _, s := range shibboleths { for i, s := range shibboleths { answer, err := ask.GetLine(s.Question + " ") if err != nil { fmt.Println("Aborted.") return 1 } if !strings.EqualFold(strings.TrimSpace(answer), strings.TrimSpace(s.Answer)) { // Rate limit: increasing delay on wrong answers. delay := time.Duration(i+1) * 2 * time.Second time.Sleep(delay) fmt.Println("Incorrect answer. Access denied.") return 1 } Loading Loading @@ -140,38 +144,37 @@ func createAccount(q *storage.Queries, fingerprint, pubkeyStr string) int { func linkAccount(q *storage.Queries, fingerprint, pubkeyStr string) int { ctx := storage.Context() login, err := ask.GetLine("Enter your existing login: ") code, err := ask.GetLine("Enter your link code (from SSH LINK command): ") if err != nil { fmt.Println("Aborted.") return 1 } login = strings.ToUpper(strings.TrimSpace(login)) // Check user exists. existing, err := q.GetUser(ctx, login) if err != nil || existing.Login == "" { fmt.Println("ERROR: User not found.") code = strings.TrimSpace(code) if code == "" { fmt.Println("ERROR: No link code provided.") return 1 } // Simple verification: ask for user's name. name, err := ask.GetLine("Enter the name on the account to verify: ") if err != nil { fmt.Println("Aborted.") return 1 } if !strings.EqualFold(strings.TrimSpace(name), strings.TrimSpace(existing.Name)) { fmt.Println("ERROR: Name does not match. Access denied.") // Clean up expired codes first. _ = q.DeleteExpiredLinkCodes(ctx) // Look up the link code. linkCode, err := q.GetLinkCode(ctx, code) if err != nil || linkCode.Code == "" { fmt.Println("ERROR: Invalid or expired link code.") return 1 } // Store the SSH key. if err := storeKey(q, fingerprint, pubkeyStr, login); err != nil { // Delete the code so it can't be reused. _ = q.DeleteLinkCode(ctx, code) // Store the SSH key for the linked account. if err := storeKey(q, fingerprint, pubkeyStr, linkCode.Login); err != nil { fmt.Printf("ERROR: Failed to store SSH key: %s\n", err) return 1 } fmt.Printf("Key linked to account %s. Reconnect to start using BULLETIN.\n", login) fmt.Printf("Key linked to account %s. Reconnect to start using BULLETIN.\n", linkCode.Login) return 0 } Loading pager/pager.go +20 −0 Original line number Diff line number Diff line Loading @@ -34,8 +34,28 @@ func readKey() (byte, error) { return buf[0], nil } // SanitizeText removes terminal escape sequences and dangerous control // characters from text, while preserving UTF-8 and normal whitespace. func SanitizeText(s string) string { var b strings.Builder b.Grow(len(s)) for _, r := range s { // Block ESC and other C0/C1 control characters except safe whitespace. if r == '\t' || r == '\n' || r == '\r' { b.WriteRune(r) continue } if r < 0x20 || (r >= 0x7f && r <= 0x9f) { continue } b.WriteRune(r) } return b.String() } // Pager pages through a string. func Pager(content string) bool { content = SanitizeText(content) lines := strings.Split(content, "\n") totalLines := len(lines) Loading repl/accounts.go +29 −0 Original line number Diff line number Diff line package repl import ( "crypto/rand" "encoding/hex" "errors" "fmt" "strings" Loading Loading @@ -369,6 +371,33 @@ func ActionSSHDelete(cmd *dclish.Command) error { return nil } // ActionSSHLink handles the `SSH LINK` command. It generates a random // link code that can be used during onboarding to link a new SSH key // to the current user's account. func ActionSSHLink(_ *dclish.Command) error { ctx := storage.Context() // Clean up expired codes first. _ = this.Q.DeleteExpiredLinkCodes(ctx) // Generate a random code. b := make([]byte, 10) if _, err := rand.Read(b); err != nil { return fmt.Errorf("failed to generate link code: %w", err) } code := hex.EncodeToString(b) // Store it. if err := this.Q.CreateLinkCode(ctx, code, this.User.Login); err != nil { return fmt.Errorf("failed to create link code: %w", err) } fmt.Printf("Your link code is: %s\n", code) fmt.Println("Use this code during onboarding to link a new SSH key to your account.") fmt.Println("This code expires in 15 minutes.") return nil } // ActionSSHFetch handles the `SSH FETCH` command. This command pulls // public keys from code sites. It's the quickest way to // add a number of keys for a user. An admin can do this Loading Loading
batch/batch.go +2 −2 Original line number Diff line number Diff line Loading @@ -217,13 +217,13 @@ Match User %s X11Forwarding no AllowTcpForwarding no PermitTunnel no ForceCommand %s --ssh ForceCommand /usr/sbin/nologin AuthorizedKeysCommand %s authorized-keys %%u %%t %%k AuthorizedKeysCommandUser %s --- Then run: sudo systemctl reload sshd --- `, os.Getenv("USER"), bulletin, bulletin, os.Getenv("USER")) `, os.Getenv("USER"), bulletin, os.Getenv("USER")) _, _ = ask.GetLine("Press Enter to continue after configuring sshd...") } Loading
key/key.go +1 −1 Original line number Diff line number Diff line Loading @@ -20,7 +20,7 @@ import ( "golang.org/x/sys/unix" ) var keytemplate = `command="%s -u %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s` var keytemplate = `command="%s --user %s",restrict %s` // Add adds an ssh key to the `authorized_keys` file. func Add(login, public string) error { Loading
onboard/onboard.go +22 −19 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import ( "fmt" "os" "strings" "time" "git.lyda.ie/pp/bulletin/ask" "git.lyda.ie/pp/bulletin/authorized" Loading Loading @@ -48,13 +49,16 @@ func Run(fingerprint, pubkeyStr string) int { if len(shibboleths) > 0 { fmt.Println("Before proceeding, please answer the following questions:") fmt.Println() for _, s := range shibboleths { for i, s := range shibboleths { answer, err := ask.GetLine(s.Question + " ") if err != nil { fmt.Println("Aborted.") return 1 } if !strings.EqualFold(strings.TrimSpace(answer), strings.TrimSpace(s.Answer)) { // Rate limit: increasing delay on wrong answers. delay := time.Duration(i+1) * 2 * time.Second time.Sleep(delay) fmt.Println("Incorrect answer. Access denied.") return 1 } Loading Loading @@ -140,38 +144,37 @@ func createAccount(q *storage.Queries, fingerprint, pubkeyStr string) int { func linkAccount(q *storage.Queries, fingerprint, pubkeyStr string) int { ctx := storage.Context() login, err := ask.GetLine("Enter your existing login: ") code, err := ask.GetLine("Enter your link code (from SSH LINK command): ") if err != nil { fmt.Println("Aborted.") return 1 } login = strings.ToUpper(strings.TrimSpace(login)) // Check user exists. existing, err := q.GetUser(ctx, login) if err != nil || existing.Login == "" { fmt.Println("ERROR: User not found.") code = strings.TrimSpace(code) if code == "" { fmt.Println("ERROR: No link code provided.") return 1 } // Simple verification: ask for user's name. name, err := ask.GetLine("Enter the name on the account to verify: ") if err != nil { fmt.Println("Aborted.") return 1 } if !strings.EqualFold(strings.TrimSpace(name), strings.TrimSpace(existing.Name)) { fmt.Println("ERROR: Name does not match. Access denied.") // Clean up expired codes first. _ = q.DeleteExpiredLinkCodes(ctx) // Look up the link code. linkCode, err := q.GetLinkCode(ctx, code) if err != nil || linkCode.Code == "" { fmt.Println("ERROR: Invalid or expired link code.") return 1 } // Store the SSH key. if err := storeKey(q, fingerprint, pubkeyStr, login); err != nil { // Delete the code so it can't be reused. _ = q.DeleteLinkCode(ctx, code) // Store the SSH key for the linked account. if err := storeKey(q, fingerprint, pubkeyStr, linkCode.Login); err != nil { fmt.Printf("ERROR: Failed to store SSH key: %s\n", err) return 1 } fmt.Printf("Key linked to account %s. Reconnect to start using BULLETIN.\n", login) fmt.Printf("Key linked to account %s. Reconnect to start using BULLETIN.\n", linkCode.Login) return 0 } Loading
pager/pager.go +20 −0 Original line number Diff line number Diff line Loading @@ -34,8 +34,28 @@ func readKey() (byte, error) { return buf[0], nil } // SanitizeText removes terminal escape sequences and dangerous control // characters from text, while preserving UTF-8 and normal whitespace. func SanitizeText(s string) string { var b strings.Builder b.Grow(len(s)) for _, r := range s { // Block ESC and other C0/C1 control characters except safe whitespace. if r == '\t' || r == '\n' || r == '\r' { b.WriteRune(r) continue } if r < 0x20 || (r >= 0x7f && r <= 0x9f) { continue } b.WriteRune(r) } return b.String() } // Pager pages through a string. func Pager(content string) bool { content = SanitizeText(content) lines := strings.Split(content, "\n") totalLines := len(lines) Loading
repl/accounts.go +29 −0 Original line number Diff line number Diff line package repl import ( "crypto/rand" "encoding/hex" "errors" "fmt" "strings" Loading Loading @@ -369,6 +371,33 @@ func ActionSSHDelete(cmd *dclish.Command) error { return nil } // ActionSSHLink handles the `SSH LINK` command. It generates a random // link code that can be used during onboarding to link a new SSH key // to the current user's account. func ActionSSHLink(_ *dclish.Command) error { ctx := storage.Context() // Clean up expired codes first. _ = this.Q.DeleteExpiredLinkCodes(ctx) // Generate a random code. b := make([]byte, 10) if _, err := rand.Read(b); err != nil { return fmt.Errorf("failed to generate link code: %w", err) } code := hex.EncodeToString(b) // Store it. if err := this.Q.CreateLinkCode(ctx, code, this.User.Login); err != nil { return fmt.Errorf("failed to create link code: %w", err) } fmt.Printf("Your link code is: %s\n", code) fmt.Println("Use this code during onboarding to link a new SSH key to your account.") fmt.Println("This code expires in 15 minutes.") return nil } // ActionSSHFetch handles the `SSH FETCH` command. This command pulls // public keys from code sites. It's the quickest way to // add a number of keys for a user. An admin can do this Loading