Files
latchd/legacy/go/auth/pam.go

229 lines
4.9 KiB
Go

// Keep each PAM transaction on one OS thread. PAM modules can use thread-local state.
package auth
/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <stdlib.h>
#include <stdint.h>
// Match the 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. Do not pass a Go pointer to PAM.
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"
}