Files
latchd/legacy/go/auth/pam.go
Zane Walker f2a885d621 refactor: split the TUI and refresh project docs
Move login state, authentication, discovery, input, rendering, and tests into focused TUI modules. Add detailed CLI error reports with recovery commands, align the build and host-check documentation, and keep the legacy Go tree outside normal builds.

Checks: make check; make integration-test; go test ./... (legacy/go).
2026-10-05 21:37:15 +05:30

229 lines
4.9 KiB
Go

// Keep each transaction on one OS thread because modules can use thread-local state.
package auth
/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <stdlib.h>
#include <stdint.h>
// Match the callback signature that cgo generates for //export.
extern int latchd_pam_conv(int, struct pam_message **,
struct pam_response **, void *);
// Store the numeric handle in C memory; never pass a Go pointer to the module.
static void *storeHandle(uintptr_t h) {
uintptr_t *p = malloc(sizeof(uintptr_t));
if (p) *p = h;
return p;
}
static uintptr_t loadHandle(void *p) {
return *(uintptr_t *)p;
}
static struct pam_conv makePAMConv(void *data) {
struct pam_conv c;
c.conv = (int (*)(int, const struct pam_message **,
struct pam_response **, void *))latchd_pam_conv;
c.appdata_ptr = data;
return c;
}
*/
import "C"
import (
"fmt"
"os"
ouser "os/user"
"runtime"
"runtime/cgo"
"strings"
"unsafe"
)
type AuthError struct {
Kind int
Msg string
}
const (
ErrPAMService = iota
ErrAccountValidation
ErrHomeDirInvalid
ErrShellInvalid
ErrUsernameNotFound
ErrSessionOpen
)
func (e *AuthError) Error() string { return e.Msg }
type Credentials struct {
Username string
UID uint32
PrimaryGID uint32
AllGIDs []uint32
HomeDir string
Shell string
pamHandle *C.pam_handle_t
pamData unsafe.Pointer
pamCB cgo.Handle
}
//export latchd_pam_conv
func latchd_pam_conv(numMsg C.int, msg **C.struct_pam_message, resp **C.struct_pam_response, data unsafe.Pointer) C.int {
h := cgo.Handle(C.loadHandle(data))
pw := h.Value().(string)
r := (*C.struct_pam_response)(C.calloc(C.ulong(numMsg), C.sizeof_struct_pam_response))
if r == nil {
return C.PAM_BUF_ERR
}
*resp = r
msgSlice := unsafe.Slice(msg, int(numMsg))
respSlice := unsafe.Slice(r, int(numMsg))
for i := 0; i < int(numMsg); i++ {
switch msgSlice[i].msg_style {
case C.PAM_PROMPT_ECHO_OFF, C.PAM_PROMPT_ECHO_ON:
c := C.CString(pw)
if c == nil {
return C.PAM_BUF_ERR
}
respSlice[i].resp = c
respSlice[i].resp_retcode = 0
default:
respSlice[i].resp = nil
respSlice[i].resp_retcode = 0
}
}
return C.PAM_SUCCESS
}
func Validate(user, pass, service string) (*Credentials, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
cSvc := C.CString(service)
cUser := C.CString(user)
defer C.free(unsafe.Pointer(cSvc))
defer C.free(unsafe.Pointer(cUser))
h := cgo.NewHandle(pass)
cData := C.storeHandle(C.uintptr_t(h))
conv := C.makePAMConv(cData)
var pamh *C.pam_handle_t
ret := C.pam_start(cSvc, cUser, &conv, &pamh)
if ret != C.PAM_SUCCESS {
h.Delete()
C.free(cData)
return nil, &AuthError{ErrPAMService, fmt.Sprintf("pam_start: %s", pamErr(pamh, ret))}
}
ret = C.pam_authenticate(pamh, 0)
if ret != C.PAM_SUCCESS {
msg := fmt.Sprintf("auth failed: %s", pamErr(pamh, ret))
C.pam_end(pamh, ret)
h.Delete()
C.free(cData)
return nil, &AuthError{ErrAccountValidation, msg}
}
ret = C.pam_acct_mgmt(pamh, 0)
if ret != C.PAM_SUCCESS {
msg := fmt.Sprintf("account: %s", pamErr(pamh, ret))
C.pam_end(pamh, ret)
h.Delete()
C.free(cData)
return nil, &AuthError{ErrAccountValidation, msg}
}
u, err := ouser.Lookup(user)
if err != nil {
C.pam_end(pamh, ret)
h.Delete()
C.free(cData)
return nil, &AuthError{ErrUsernameNotFound, fmt.Sprintf("lookup %s: %v", user, err)}
}
uid := atou32(u.Uid)
gid := atou32(u.Gid)
gids := []uint32{gid}
if ids, err := u.GroupIds(); err == nil {
for _, s := range ids {
g := atou32(s)
if g != gid {
gids = append(gids, g)
}
}
}
return &Credentials{
Username: user,
UID: uid,
PrimaryGID: gid,
AllGIDs: gids,
HomeDir: u.HomeDir,
Shell: shellFromPasswd(uid),
pamHandle: pamh,
pamData: cData,
pamCB: h,
}, nil
}
func (c *Credentials) OpenSession() error {
ret := C.pam_open_session(c.pamHandle, 0)
if ret != C.PAM_SUCCESS {
return &AuthError{ErrSessionOpen, fmt.Sprintf("session: %s", pamErr(c.pamHandle, ret))}
}
return nil
}
func (c *Credentials) CloseSession() {
if c.pamHandle != nil {
C.pam_close_session(c.pamHandle, 0)
C.pam_end(c.pamHandle, 0)
c.pamHandle = nil
}
if c.pamData != nil {
C.free(c.pamData)
c.pamData = nil
}
c.pamCB.Delete()
}
func pamErr(h *C.pam_handle_t, e C.int) string {
return C.GoString(C.pam_strerror(h, e))
}
func atou32(s string) uint32 {
var n uint32
for _, c := range s {
if c < '0' || c > '9' {
break
}
n = n*10 + uint32(c-'0')
}
return n
}
func shellFromPasswd(uid uint32) string {
data, err := os.ReadFile("/etc/passwd")
if err != nil {
return "/bin/sh"
}
uidStr := fmt.Sprint(uid)
for _, line := range strings.Split(string(data), "\n") {
parts := strings.Split(line, ":")
if len(parts) >= 7 && parts[2] == uidStr && parts[6] != "" {
sh := parts[6]
if sh != "/sbin/nologin" && sh != "/usr/sbin/nologin" && sh != "/bin/false" {
return sh
}
}
}
return "/bin/sh"
}