blob: 5c2e9d79a1b930626036e09c9f762bbdc792bcc8 [file] [log] [blame]
// Package keytab implements Kerberos keytabs: https://web.mit.edu/kerberos/krb5-devel/doc/formats/keytab_file_format.html.
package keytab
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
"time"
"unsafe"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/types"
)
const (
keytabFirstByte byte = 05
)
// Keytab struct.
type Keytab struct {
version uint8
Entries []entry
}
// Keytab entry struct.
type entry struct {
Principal principal
Timestamp time.Time
KVNO8 uint8
Key types.EncryptionKey
KVNO uint32
}
func (e entry) String() string {
return fmt.Sprintf("% 4d %s %-56s %2d %-64x",
e.KVNO8,
e.Timestamp.Format("02/01/06 15:04:05"),
e.Principal.String(),
e.Key.KeyType,
e.Key.KeyValue,
)
}
// Keytab entry principal struct.
type principal struct {
NumComponents int16 `json:"-"`
Realm string
Components []string
NameType int32
}
func (p principal) String() string {
return fmt.Sprintf("%s@%s", strings.Join(p.Components, "/"), p.Realm)
}
// New creates new, empty Keytab type.
func New() *Keytab {
var e []entry
return &Keytab{
version: 2,
Entries: e,
}
}
// GetEncryptionKey returns the EncryptionKey from the Keytab for the newest entry with the required kvno, etype and matching principal.
// If the kvno is zero then the latest kvno will be returned. The kvno is also returned for
func (kt *Keytab) GetEncryptionKey(princName types.PrincipalName, realm string, kvno int, etype int32) (types.EncryptionKey, int, error) {
var key types.EncryptionKey
var t time.Time
var kv int
for _, k := range kt.Entries {
if k.Principal.Realm == realm && len(k.Principal.Components) == len(princName.NameString) &&
k.Key.KeyType == etype &&
(k.KVNO == uint32(kvno) || kvno == 0) &&
k.Timestamp.After(t) {
p := true
for i, n := range k.Principal.Components {
if princName.NameString[i] != n {
p = false
break
}
}
if p {
key = k.Key
kv = int(k.KVNO)
t = k.Timestamp
}
}
}
if len(key.KeyValue) < 1 {
return key, 0, fmt.Errorf("matching key not found in keytab. Looking for %v realm: %v kvno: %v etype: %v", princName.NameString, realm, kvno, etype)
}
return key, kv, nil
}
// Create a new Keytab entry.
func newEntry() entry {
var b []byte
return entry{
Principal: newPrincipal(),
Timestamp: time.Time{},
KVNO8: 0,
Key: types.EncryptionKey{
KeyType: 0,
KeyValue: b,
},
KVNO: 0,
}
}
func (kt Keytab) String() string {
var s string
s = `KVNO Timestamp Principal ET Key
---- ----------------- -------------------------------------------------------- -- ----------------------------------------------------------------
`
for _, entry := range kt.Entries {
s += entry.String() + "\n"
}
return s
}
// AddEntry adds an entry to the keytab. The password should be provided in plain text and it will be converted using the defined enctype to be stored.
func (kt *Keytab) AddEntry(principalName, realm, password string, ts time.Time, KVNO uint8, encType int32) error {
// Generate a key from the password
princ, _ := types.ParseSPNString(principalName)
key, _, err := crypto.GetKeyFromPassword(password, princ, realm, encType, types.PADataSequence{})
if err != nil {
return err
}
// Populate the keytab entry principal
ktep := newPrincipal()
ktep.NumComponents = int16(len(princ.NameString))
if kt.version == 1 {
ktep.NumComponents += 1
}
ktep.Realm = realm
ktep.Components = princ.NameString
ktep.NameType = princ.NameType
// Populate the keytab entry
e := newEntry()
e.Principal = ktep
e.Timestamp = ts
e.KVNO8 = KVNO
e.KVNO = uint32(KVNO)
e.Key = key
kt.Entries = append(kt.Entries, e)
return nil
}
// Create a new principal.
func newPrincipal() principal {
var c []string
return principal{
NumComponents: 0,
Realm: "",
Components: c,
NameType: 0,
}
}
// Load a Keytab file into a Keytab type.
func Load(ktPath string) (*Keytab, error) {
kt := new(Keytab)
b, err := ioutil.ReadFile(ktPath)
if err != nil {
return kt, err
}
err = kt.Unmarshal(b)
return kt, err
}
// Marshal keytab into byte slice
func (kt *Keytab) Marshal() ([]byte, error) {
b := []byte{keytabFirstByte, kt.version}
for _, e := range kt.Entries {
eb, err := e.marshal(int(kt.version))
if err != nil {
return b, err
}
b = append(b, eb...)
}
return b, nil
}
// Write the keytab bytes to io.Writer.
// Returns the number of bytes written
func (kt *Keytab) Write(w io.Writer) (int, error) {
b, err := kt.Marshal()
if err != nil {
return 0, fmt.Errorf("error marshaling keytab: %v", err)
}
return w.Write(b)
}
// Unmarshal byte slice of Keytab data into Keytab type.
func (kt *Keytab) Unmarshal(b []byte) error {
if len(b) < 2 {
return fmt.Errorf("byte array is less than 2 bytes: %d", len(b))
}
//The first byte of the file always has the value 5
if b[0] != keytabFirstByte {
return errors.New("invalid keytab data. First byte does not equal 5")
}
//Get keytab version
//The 2nd byte contains the version number (1 or 2)
kt.version = b[1]
if kt.version != 1 && kt.version != 2 {
return errors.New("invalid keytab data. Keytab version is neither 1 nor 2")
}
//Version 1 of the file format uses native byte order for integer representations. Version 2 always uses big-endian byte order
var endian binary.ByteOrder
endian = binary.BigEndian
if kt.version == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
// n tracks position in the byte array
n := 2
l, err := readInt32(b, &n, &endian)
if err != nil {
return err
}
for l != 0 {
if l < 0 {
//Zero padded so skip over
l = l * -1
n = n + int(l)
} else {
if n < 0 {
return fmt.Errorf("%d can't be less than zero", n)
}
if n+int(l) > len(b) {
return fmt.Errorf("%s's length is less than %d", b, n+int(l))
}
eb := b[n : n+int(l)]
n = n + int(l)
ke := newEntry()
// p keeps track as to where we are in the byte stream
var p int
var err error
parsePrincipal(eb, &p, kt, &ke, &endian)
ke.Timestamp, err = readTimestamp(eb, &p, &endian)
if err != nil {
return err
}
rei8, err := readInt8(eb, &p, &endian)
if err != nil {
return err
}
ke.KVNO8 = uint8(rei8)
rei16, err := readInt16(eb, &p, &endian)
if err != nil {
return err
}
ke.Key.KeyType = int32(rei16)
rei16, err = readInt16(eb, &p, &endian)
if err != nil {
return err
}
kl := int(rei16)
ke.Key.KeyValue, err = readBytes(eb, &p, kl, &endian)
if err != nil {
return err
}
// The 32-bit key version overrides the 8-bit key version.
// If at least 4 bytes are left after the other fields are read and they are non-zero
// this indicates the 32-bit version is present.
if len(eb)-p >= 4 {
// The 32-bit key may be present
ri32, err := readInt32(eb, &p, &endian)
if err != nil {
return err
}
ke.KVNO = uint32(ri32)
}
if ke.KVNO == 0 {
// Handles if the value from the last 4 bytes was zero and also if there are not the 4 bytes present. Makes sense to put the same value here as KVNO8
ke.KVNO = uint32(ke.KVNO8)
}
// Add the entry to the keytab
kt.Entries = append(kt.Entries, ke)
}
// Check if there are still 4 bytes left to read
// Also check that n is greater than zero
if n < 0 || n > len(b) || len(b[n:]) < 4 {
break
}
// Read the size of the next entry
l, err = readInt32(b, &n, &endian)
if err != nil {
return err
}
}
return nil
}
func (e entry) marshal(v int) ([]byte, error) {
var b []byte
pb, err := e.Principal.marshal(v)
if err != nil {
return b, err
}
b = append(b, pb...)
var endian binary.ByteOrder
endian = binary.BigEndian
if v == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
t := make([]byte, 9)
endian.PutUint32(t[0:4], uint32(e.Timestamp.Unix()))
t[4] = e.KVNO8
endian.PutUint16(t[5:7], uint16(e.Key.KeyType))
endian.PutUint16(t[7:9], uint16(len(e.Key.KeyValue)))
b = append(b, t...)
buf := new(bytes.Buffer)
err = binary.Write(buf, endian, e.Key.KeyValue)
if err != nil {
return b, err
}
b = append(b, buf.Bytes()...)
t = make([]byte, 4)
endian.PutUint32(t, e.KVNO)
b = append(b, t...)
// Add the length header
t = make([]byte, 4)
endian.PutUint32(t, uint32(len(b)))
b = append(t, b...)
return b, nil
}
// Parse the Keytab bytes of a principal into a Keytab entry's principal.
func parsePrincipal(b []byte, p *int, kt *Keytab, ke *entry, e *binary.ByteOrder) error {
var err error
ke.Principal.NumComponents, err = readInt16(b, p, e)
if err != nil {
return err
}
if kt.version == 1 {
//In version 1 the number of components includes the realm. Minus 1 to make consistent with version 2
ke.Principal.NumComponents--
}
lenRealm, err := readInt16(b, p, e)
if err != nil {
return err
}
realmB, err := readBytes(b, p, int(lenRealm), e)
if err != nil {
return err
}
ke.Principal.Realm = string(realmB)
for i := 0; i < int(ke.Principal.NumComponents); i++ {
l, err := readInt16(b, p, e)
if err != nil {
return err
}
compB, err := readBytes(b, p, int(l), e)
if err != nil {
return err
}
ke.Principal.Components = append(ke.Principal.Components, string(compB))
}
if kt.version != 1 {
//Name Type is omitted in version 1
ke.Principal.NameType, err = readInt32(b, p, e)
if err != nil {
return err
}
}
return nil
}
func (p principal) marshal(v int) ([]byte, error) {
//var b []byte
b := make([]byte, 2)
var endian binary.ByteOrder
endian = binary.BigEndian
if v == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
endian.PutUint16(b[0:], uint16(p.NumComponents))
realm, err := marshalString(p.Realm, v)
if err != nil {
return b, err
}
b = append(b, realm...)
for _, c := range p.Components {
cb, err := marshalString(c, v)
if err != nil {
return b, err
}
b = append(b, cb...)
}
if v != 1 {
t := make([]byte, 4)
endian.PutUint32(t, uint32(p.NameType))
b = append(b, t...)
}
return b, nil
}
func marshalString(s string, v int) ([]byte, error) {
sb := []byte(s)
b := make([]byte, 2)
var endian binary.ByteOrder
endian = binary.BigEndian
if v == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
endian.PutUint16(b[0:], uint16(len(sb)))
buf := new(bytes.Buffer)
err := binary.Write(buf, endian, sb)
if err != nil {
return b, err
}
b = append(b, buf.Bytes()...)
return b, err
}
// Read bytes representing a timestamp.
func readTimestamp(b []byte, p *int, e *binary.ByteOrder) (time.Time, error) {
i32, err := readInt32(b, p, e)
if err != nil {
return time.Time{}, err
}
return time.Unix(int64(i32), 0), nil
}
// Read bytes representing an eight bit integer.
func readInt8(b []byte, p *int, e *binary.ByteOrder) (i int8, err error) {
if *p < 0 {
return 0, fmt.Errorf("%d cannot be less than zero", *p)
}
if (*p + 1) > len(b) {
return 0, fmt.Errorf("%s's length is less than %d", b, *p+1)
}
buf := bytes.NewBuffer(b[*p : *p+1])
binary.Read(buf, *e, &i)
*p++
return
}
// Read bytes representing a sixteen bit integer.
func readInt16(b []byte, p *int, e *binary.ByteOrder) (i int16, err error) {
if *p < 0 {
return 0, fmt.Errorf("%d cannot be less than zero", *p)
}
if (*p + 2) > len(b) {
return 0, fmt.Errorf("%s's length is less than %d", b, *p+2)
}
buf := bytes.NewBuffer(b[*p : *p+2])
binary.Read(buf, *e, &i)
*p += 2
return
}
// Read bytes representing a thirty two bit integer.
func readInt32(b []byte, p *int, e *binary.ByteOrder) (i int32, err error) {
if *p < 0 {
return 0, fmt.Errorf("%d cannot be less than zero", *p)
}
if (*p + 4) > len(b) {
return 0, fmt.Errorf("%s's length is less than %d", b, *p+4)
}
buf := bytes.NewBuffer(b[*p : *p+4])
binary.Read(buf, *e, &i)
*p += 4
return
}
func readBytes(b []byte, p *int, s int, e *binary.ByteOrder) ([]byte, error) {
if s < 0 {
return nil, fmt.Errorf("%d cannot be less than zero", s)
}
i := *p + s
if i > len(b) {
return nil, fmt.Errorf("%s's length is greater than %d", b, i)
}
buf := bytes.NewBuffer(b[*p:i])
r := make([]byte, s)
if err := binary.Read(buf, *e, &r); err != nil {
return nil, err
}
*p += s
return r, nil
}
func isNativeEndianLittle() bool {
var x = 0x012345678
var p = unsafe.Pointer(&x)
var bp = (*[4]byte)(p)
var endian bool
if 0x01 == bp[0] {
endian = false
} else if (0x78 & 0xff) == (bp[0] & 0xff) {
endian = true
} else {
// Default to big endian
endian = false
}
return endian
}
// JSON return information about the keys held in the keytab in a JSON format.
func (kt *Keytab) JSON() (string, error) {
b, err := json.MarshalIndent(kt, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}