package client
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/crypto/etype"
"github.com/jcmturner/gokrb5/v8/iana/errorcode"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/jcmturner/gokrb5/v8/krberror"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
)
type Client struct {
Credentials *credentials .Credentials
Config *config .Config
settings *Settings
sessions *sessions
cache *Cache
}
func NewWithPassword (username , realm , password string , krb5conf *config .Config , settings ...func (*Settings )) *Client {
creds := credentials .New (username , realm )
return &Client {
Credentials : creds .WithPassword (password ),
Config : krb5conf ,
settings : NewSettings (settings ...),
sessions : &sessions {
Entries : make (map [string ]*session ),
},
cache : NewCache (),
}
}
func NewWithKeytab (username , realm string , kt *keytab .Keytab , krb5conf *config .Config , settings ...func (*Settings )) *Client {
creds := credentials .New (username , realm )
return &Client {
Credentials : creds .WithKeytab (kt ),
Config : krb5conf ,
settings : NewSettings (settings ...),
sessions : &sessions {
Entries : make (map [string ]*session ),
},
cache : NewCache (),
}
}
func NewFromCCache (c *credentials .CCache , krb5conf *config .Config , settings ...func (*Settings )) (*Client , error ) {
cl := &Client {
Credentials : c .GetClientCredentials (),
Config : krb5conf ,
settings : NewSettings (settings ...),
sessions : &sessions {
Entries : make (map [string ]*session ),
},
cache : NewCache (),
}
spn := types .PrincipalName {
NameType : nametype .KRB_NT_SRV_INST ,
NameString : []string {"krbtgt" , c .DefaultPrincipal .Realm },
}
cred , ok := c .GetEntry (spn )
if !ok {
return cl , errors .New ("TGT not found in CCache" )
}
var tgt messages .Ticket
err := tgt .Unmarshal (cred .Ticket )
if err != nil {
return cl , fmt .Errorf ("TGT bytes in cache are not valid: %v" , err )
}
cl .sessions .Entries [c .DefaultPrincipal .Realm ] = &session {
realm : c .DefaultPrincipal .Realm ,
authTime : cred .AuthTime ,
endTime : cred .EndTime ,
renewTill : cred .RenewTill ,
tgt : tgt ,
sessionKey : cred .Key ,
}
for _ , cred := range c .GetEntries () {
var tkt messages .Ticket
err = tkt .Unmarshal (cred .Ticket )
if err != nil {
return cl , fmt .Errorf ("cache entry ticket bytes are not valid: %v" , err )
}
cl .cache .addEntry (
tkt ,
cred .AuthTime ,
cred .StartTime ,
cred .EndTime ,
cred .RenewTill ,
cred .Key ,
)
}
return cl , nil
}
func (cl *Client ) Key (etype etype .EType , kvno int , krberr *messages .KRBError ) (types .EncryptionKey , int , error ) {
if cl .Credentials .HasKeytab () && etype != nil {
return cl .Credentials .Keytab ().GetEncryptionKey (cl .Credentials .CName (), cl .Credentials .Domain (), kvno , etype .GetETypeID ())
} else if cl .Credentials .HasPassword () {
if krberr != nil && krberr .ErrorCode == errorcode .KDC_ERR_PREAUTH_REQUIRED {
var pas types .PADataSequence
err := pas .Unmarshal (krberr .EData )
if err != nil {
return types .EncryptionKey {}, 0 , fmt .Errorf ("could not get PAData from KRBError to generate key from password: %v" , err )
}
key , _ , err := crypto .GetKeyFromPassword (cl .Credentials .Password (), krberr .CName , krberr .CRealm , etype .GetETypeID (), pas )
return key , 0 , err
}
key , _ , err := crypto .GetKeyFromPassword (cl .Credentials .Password (), cl .Credentials .CName (), cl .Credentials .Domain (), etype .GetETypeID (), types .PADataSequence {})
return key , 0 , err
}
return types .EncryptionKey {}, 0 , errors .New ("credential has neither keytab or password to generate key" )
}
func (cl *Client ) IsConfigured () (bool , error ) {
if cl .Credentials .UserName () == "" {
return false , errors .New ("client does not have a username" )
}
if cl .Credentials .Domain () == "" {
return false , errors .New ("client does not have a define realm" )
}
if !cl .Credentials .HasPassword () && !cl .Credentials .HasKeytab () {
authTime , _ , _ , _ , err := cl .sessionTimes (cl .Credentials .Domain ())
if err != nil || authTime .IsZero () {
return false , errors .New ("client has neither a keytab nor a password set and no session" )
}
}
if !cl .Config .LibDefaults .DNSLookupKDC {
for _ , r := range cl .Config .Realms {
if r .Realm == cl .Credentials .Domain () {
if len (r .KDC ) > 0 {
return true , nil
}
return false , errors .New ("client krb5 config does not have any defined KDCs for the default realm" )
}
}
}
return true , nil
}
func (cl *Client ) Login () error {
if ok , err := cl .IsConfigured (); !ok {
return err
}
if !cl .Credentials .HasPassword () && !cl .Credentials .HasKeytab () {
_ , endTime , _ , _ , err := cl .sessionTimes (cl .Credentials .Domain ())
if err != nil {
return krberror .Errorf (err , krberror .KRBMsgError , "no user credentials available and error getting any existing session" )
}
if time .Now ().UTC ().After (endTime ) {
return krberror .New (krberror .KRBMsgError , "cannot login, no user credentials available and no valid existing session" )
}
return nil
}
ASReq , err := messages .NewASReqForTGT (cl .Credentials .Domain (), cl .Config , cl .Credentials .CName ())
if err != nil {
return krberror .Errorf (err , krberror .KRBMsgError , "error generating new AS_REQ" )
}
ASRep , err := cl .ASExchange (cl .Credentials .Domain (), ASReq , 0 )
if err != nil {
return err
}
cl .addSession (ASRep .Ticket , ASRep .DecryptedEncPart )
return nil
}
func (cl *Client ) AffirmLogin () error {
_ , endTime , _ , _ , err := cl .sessionTimes (cl .Credentials .Domain ())
if err != nil || time .Now ().UTC ().After (endTime ) {
err := cl .Login ()
if err != nil {
return fmt .Errorf ("could not get valid TGT for client's realm: %v" , err )
}
}
return nil
}
func (cl *Client ) realmLogin (realm string ) error {
if realm == cl .Credentials .Domain () {
return cl .Login ()
}
_ , endTime , _ , _ , err := cl .sessionTimes (cl .Credentials .Domain ())
if err != nil || time .Now ().UTC ().After (endTime ) {
err := cl .Login ()
if err != nil {
return fmt .Errorf ("could not get valid TGT for client's realm: %v" , err )
}
}
tgt , skey , err := cl .sessionTGT (cl .Credentials .Domain ())
if err != nil {
return err
}
spn := types .PrincipalName {
NameType : nametype .KRB_NT_SRV_INST ,
NameString : []string {"krbtgt" , realm },
}
_ , tgsRep , err := cl .TGSREQGenerateAndExchange (spn , cl .Credentials .Domain (), tgt , skey , false )
if err != nil {
return err
}
cl .addSession (tgsRep .Ticket , tgsRep .DecryptedEncPart )
return nil
}
func (cl *Client ) Destroy () {
creds := credentials .New ("" , "" )
cl .sessions .destroy ()
cl .cache .clear ()
cl .Credentials = creds
cl .Log ("client destroyed" )
}
func (cl *Client ) Diagnostics (w io .Writer ) error {
cl .Print (w )
var errs []string
if cl .Credentials .HasKeytab () {
var loginRealmEncTypes []int32
for _ , e := range cl .Credentials .Keytab ().Entries {
if e .Principal .Realm == cl .Credentials .Realm () {
loginRealmEncTypes = append (loginRealmEncTypes , e .Key .KeyType )
}
}
for _ , et := range cl .Config .LibDefaults .DefaultTktEnctypeIDs {
var etInKt bool
for _ , val := range loginRealmEncTypes {
if val == et {
etInKt = true
break
}
}
if !etInKt {
errs = append (errs , fmt .Sprintf ("default_tkt_enctypes specifies %d but this enctype is not available in the client's keytab" , et ))
}
}
for _ , et := range cl .Config .LibDefaults .PreferredPreauthTypes {
var etInKt bool
for _ , val := range loginRealmEncTypes {
if int (val ) == et {
etInKt = true
break
}
}
if !etInKt {
errs = append (errs , fmt .Sprintf ("preferred_preauth_types specifies %d but this enctype is not available in the client's keytab" , et ))
}
}
}
udpCnt , udpKDC , err := cl .Config .GetKDCs (cl .Credentials .Realm (), false )
if err != nil {
errs = append (errs , fmt .Sprintf ("error when resolving KDCs for UDP communication: %v" , err ))
}
if udpCnt < 1 {
errs = append (errs , "no KDCs resolved for communication via UDP." )
} else {
b , _ := json .MarshalIndent (&udpKDC , "" , " " )
fmt .Fprintf (w , "UDP KDCs: %s\n" , string (b ))
}
tcpCnt , tcpKDC , err := cl .Config .GetKDCs (cl .Credentials .Realm (), false )
if err != nil {
errs = append (errs , fmt .Sprintf ("error when resolving KDCs for TCP communication: %v" , err ))
}
if tcpCnt < 1 {
errs = append (errs , "no KDCs resolved for communication via TCP." )
} else {
b , _ := json .MarshalIndent (&tcpKDC , "" , " " )
fmt .Fprintf (w , "TCP KDCs: %s\n" , string (b ))
}
if errs == nil || len (errs ) < 1 {
return nil
}
err = fmt .Errorf (strings .Join (errs , "\n" ))
return err
}
func (cl *Client ) Print (w io .Writer ) {
c , _ := cl .Credentials .JSON ()
fmt .Fprintf (w , "Credentials:\n%s\n" , c )
s , _ := cl .sessions .JSON ()
fmt .Fprintf (w , "TGT Sessions:\n%s\n" , s )
c , _ = cl .cache .JSON ()
fmt .Fprintf (w , "Service ticket cache:\n%s\n" , c )
s , _ = cl .settings .JSON ()
fmt .Fprintf (w , "Settings:\n%s\n" , s )
j , _ := cl .Config .JSON ()
fmt .Fprintf (w , "Krb5 config:\n%s\n" , j )
k , _ := cl .Credentials .Keytab ().JSON ()
fmt .Fprintf (w , "Keytab:\n%s\n" , k )
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .