// Package keytab implements Kerberos keytabs: https://web.mit.edu/kerberos/krb5-devel/doc/formats/keytab_file_format.html.
package keytab import ( ) 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 ( entry) () string { return fmt.Sprintf("% 4d %s %-56s %2d %-64x", .KVNO8, .Timestamp.Format("02/01/06 15:04:05"), .Principal.String(), .Key.KeyType, .Key.KeyValue, ) } // Keytab entry principal struct. type principal struct { NumComponents int16 `json:"-"` Realm string Components []string NameType int32 } func ( principal) () string { return fmt.Sprintf("%s@%s", strings.Join(.Components, "/"), .Realm) } // New creates new, empty Keytab type. func () *Keytab { var []entry return &Keytab{ version: 2, Entries: , } } // 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 ( *Keytab) ( types.PrincipalName, string, int, int32) (types.EncryptionKey, int, error) { var types.EncryptionKey var time.Time var int for , := range .Entries { if .Principal.Realm == && len(.Principal.Components) == len(.NameString) && .Key.KeyType == && (.KVNO == uint32() || == 0) && .Timestamp.After() { := true for , := range .Principal.Components { if .NameString[] != { = false break } } if { = .Key = int(.KVNO) = .Timestamp } } } if len(.KeyValue) < 1 { return , 0, fmt.Errorf("matching key not found in keytab. Looking for %q realm: %v kvno: %v etype: %v", .PrincipalNameString(), , , ) } return , , nil } // Create a new Keytab entry. func newEntry() entry { var []byte return entry{ Principal: newPrincipal(), Timestamp: time.Time{}, KVNO8: 0, Key: types.EncryptionKey{ KeyType: 0, KeyValue: , }, KVNO: 0, } } func ( Keytab) () string { var string = `KVNO Timestamp Principal ET Key ---- ----------------- -------------------------------------------------------- -- ---------------------------------------------------------------- ` for , := range .Entries { += .String() + "\n" } return } // 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 ( *Keytab) (, , string, time.Time, uint8, int32) error { // Generate a key from the password , := types.ParseSPNString() , , := crypto.GetKeyFromPassword(, , , , types.PADataSequence{}) if != nil { return } // Populate the keytab entry principal := newPrincipal() .NumComponents = int16(len(.NameString)) if .version == 1 { .NumComponents += 1 } .Realm = .Components = .NameString .NameType = .NameType // Populate the keytab entry := newEntry() .Principal = .Timestamp = .KVNO8 = .KVNO = uint32() .Key = .Entries = append(.Entries, ) return nil } // Create a new principal. func newPrincipal() principal { var []string return principal{ NumComponents: 0, Realm: "", Components: , NameType: 0, } } // Load a Keytab file into a Keytab type. func ( string) (*Keytab, error) { := new(Keytab) , := os.ReadFile() if != nil { return , } = .Unmarshal() return , } // Marshal keytab into byte slice func ( *Keytab) () ([]byte, error) { := []byte{keytabFirstByte, .version} for , := range .Entries { , := .marshal(int(.version)) if != nil { return , } = append(, ...) } return , nil } // Write the keytab bytes to io.Writer. // Returns the number of bytes written func ( *Keytab) ( io.Writer) (int, error) { , := .Marshal() if != nil { return 0, fmt.Errorf("error marshaling keytab: %v", ) } return .Write() } // Unmarshal byte slice of Keytab data into Keytab type. func ( *Keytab) ( []byte) error { if len() < 2 { return fmt.Errorf("byte array is less than 2 bytes: %d", len()) } //The first byte of the file always has the value 5 if [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) .version = [1] if .version != 1 && .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 binary.ByteOrder = binary.BigEndian if .version == 1 && isNativeEndianLittle() { = binary.LittleEndian } // n tracks position in the byte array := 2 , := readInt32(, &, &) if != nil { return } for != 0 { if < 0 { //Zero padded so skip over = * -1 = + int() } else { if < 0 { return fmt.Errorf("%d can't be less than zero", ) } if +int() > len() { return fmt.Errorf("%s's length is less than %d", , +int()) } := [ : +int()] = + int() := newEntry() // p keeps track as to where we are in the byte stream var int var error parsePrincipal(, &, , &, &) .Timestamp, = readTimestamp(, &, &) if != nil { return } , := readInt8(, &, &) if != nil { return } .KVNO8 = uint8() , := readInt16(, &, &) if != nil { return } .Key.KeyType = int32() , = readInt16(, &, &) if != nil { return } := int() .Key.KeyValue, = readBytes(, &, , &) if != nil { return } // 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()- >= 4 { // The 32-bit key may be present , := readInt32(, &, &) if != nil { return } .KVNO = uint32() } if .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 .KVNO = uint32(.KVNO8) } // Add the entry to the keytab .Entries = append(.Entries, ) } // Check if there are still 4 bytes left to read // Also check that n is greater than zero if < 0 || > len() || len([:]) < 4 { break } // Read the size of the next entry , = readInt32(, &, &) if != nil { return } } return nil } func ( entry) ( int) ([]byte, error) { var []byte , := .Principal.marshal() if != nil { return , } = append(, ...) var binary.ByteOrder = binary.BigEndian if == 1 && isNativeEndianLittle() { = binary.LittleEndian } := make([]byte, 9) .PutUint32([0:4], uint32(.Timestamp.Unix())) [4] = .KVNO8 .PutUint16([5:7], uint16(.Key.KeyType)) .PutUint16([7:9], uint16(len(.Key.KeyValue))) = append(, ...) := new(bytes.Buffer) = binary.Write(, , .Key.KeyValue) if != nil { return , } = append(, .Bytes()...) = make([]byte, 4) .PutUint32(, .KVNO) = append(, ...) // Add the length header = make([]byte, 4) .PutUint32(, uint32(len())) = append(, ...) return , nil } // Parse the Keytab bytes of a principal into a Keytab entry's principal. func parsePrincipal( []byte, *int, *Keytab, *entry, *binary.ByteOrder) error { var error .Principal.NumComponents, = readInt16(, , ) if != nil { return } if .version == 1 { //In version 1 the number of components includes the realm. Minus 1 to make consistent with version 2 .Principal.NumComponents-- } , := readInt16(, , ) if != nil { return } , := readBytes(, , int(), ) if != nil { return } .Principal.Realm = string() for := 0; < int(.Principal.NumComponents); ++ { , := readInt16(, , ) if != nil { return } , := readBytes(, , int(), ) if != nil { return } .Principal.Components = append(.Principal.Components, string()) } if .version != 1 { //Name Type is omitted in version 1 .Principal.NameType, = readInt32(, , ) if != nil { return } } return nil } func ( principal) ( int) ([]byte, error) { //var b []byte := make([]byte, 2) var binary.ByteOrder = binary.BigEndian if == 1 && isNativeEndianLittle() { = binary.LittleEndian } .PutUint16([0:], uint16(.NumComponents)) , := marshalString(.Realm, ) if != nil { return , } = append(, ...) for , := range .Components { , := marshalString(, ) if != nil { return , } = append(, ...) } if != 1 { := make([]byte, 4) .PutUint32(, uint32(.NameType)) = append(, ...) } return , nil } func marshalString( string, int) ([]byte, error) { := []byte() := make([]byte, 2) var binary.ByteOrder = binary.BigEndian if == 1 && isNativeEndianLittle() { = binary.LittleEndian } .PutUint16([0:], uint16(len())) := new(bytes.Buffer) := binary.Write(, , ) if != nil { return , } = append(, .Bytes()...) return , } // Read bytes representing a timestamp. func readTimestamp( []byte, *int, *binary.ByteOrder) (time.Time, error) { , := readInt32(, , ) if != nil { return time.Time{}, } return time.Unix(int64(), 0), nil } // Read bytes representing an eight bit integer. func readInt8( []byte, *int, *binary.ByteOrder) ( int8, error) { if * < 0 { return 0, fmt.Errorf("%d cannot be less than zero", *) } if (* + 1) > len() { return 0, fmt.Errorf("%s's length is less than %d", , *+1) } := bytes.NewBuffer([* : *+1]) binary.Read(, *, &) *++ return } // Read bytes representing a sixteen bit integer. func readInt16( []byte, *int, *binary.ByteOrder) ( int16, error) { if * < 0 { return 0, fmt.Errorf("%d cannot be less than zero", *) } if (* + 2) > len() { return 0, fmt.Errorf("%s's length is less than %d", , *+2) } := bytes.NewBuffer([* : *+2]) binary.Read(, *, &) * += 2 return } // Read bytes representing a thirty two bit integer. func readInt32( []byte, *int, *binary.ByteOrder) ( int32, error) { if * < 0 { return 0, fmt.Errorf("%d cannot be less than zero", *) } if (* + 4) > len() { return 0, fmt.Errorf("%s's length is less than %d", , *+4) } := bytes.NewBuffer([* : *+4]) binary.Read(, *, &) * += 4 return } func readBytes( []byte, *int, int, *binary.ByteOrder) ([]byte, error) { if < 0 { return nil, fmt.Errorf("%d cannot be less than zero", ) } := * + if > len() { return nil, fmt.Errorf("%s's length is greater than %d", , ) } := bytes.NewBuffer([*:]) := make([]byte, ) if := binary.Read(, *, &); != nil { return nil, } * += return , nil } func isNativeEndianLittle() bool { var = 0x012345678 var = unsafe.Pointer(&) var = (*[4]byte)() var bool if 0x01 == [0] { = false } else if (0x78 & 0xff) == ([0] & 0xff) { = true } else { // Default to big endian = false } return } // JSON return information about the keys held in the keytab in a JSON format. func ( *Keytab) () (string, error) { , := json.MarshalIndent(, "", " ") if != nil { return "", } return string(), nil }