| // Package keytab implements Kerberos keytabs: https://web.mit.edu/kerberos/krb5-devel/doc/formats/keytab_file_format.html. |
| package keytab |
| |
| import ( |
| "bytes" |
| "encoding/binary" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "time" |
| "unsafe" |
| |
| "gopkg.in/jcmturner/gokrb5.v7/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 |
| } |
| |
| // Keytab entry principal struct. |
| type principal struct { |
| NumComponents int16 |
| Realm string |
| Components []string |
| NameType int32 |
| } |
| |
| // New creates new, empty Keytab type. |
| func New() *Keytab { |
| var e []entry |
| return &Keytab{ |
| version: 0, |
| Entries: e, |
| } |
| } |
| |
| // GetEncryptionKey returns the EncryptionKey from the Keytab for the newest entry with the required kvno, etype and matching principal. |
| func (kt *Keytab) GetEncryptionKey(princName types.PrincipalName, realm string, kvno int, etype int32) (types.EncryptionKey, error) { |
| //TODO (theme: KVNO from keytab) this function should return the kvno too |
| var key types.EncryptionKey |
| var t time.Time |
| 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 |
| t = k.Timestamp |
| } |
| } |
| } |
| if len(key.KeyValue) < 1 { |
| return key, fmt.Errorf("matching key not found in keytab. Looking for %v realm: %v kvno: %v etype: %v", princName.NameString, realm, kvno, etype) |
| } |
| return key, nil |
| } |
| |
| // Create a new Keytab entry. |
| func newKeytabEntry() entry { |
| var b []byte |
| return entry{ |
| Principal: newPrincipal(), |
| Timestamp: time.Time{}, |
| KVNO8: 0, |
| Key: types.EncryptionKey{ |
| KeyType: 0, |
| KeyValue: b, |
| }, |
| KVNO: 0, |
| } |
| } |
| |
| // 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 |
| } |
| /* |
| After the two-byte version indicator, the file contains a sequence of signed 32-bit record lengths followed by key records or holes. |
| A positive record length indicates a valid key entry whose size is equal to or less than the record length. |
| A negative length indicates a zero-filled hole whose size is the inverse of the length. |
| A length of 0 indicates the end of the file. |
| */ |
| // 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 { |
| //fmt.Printf("Bytes for entry: %v\n", b[n:n+int(l)]) |
| 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 := newKeytabEntry() |
| // 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. |
| // To determine if it is present, the implementation must check that at least 4 bytes remain in the record after the other fields are read, |
| // and that the value of the 32-bit integer contained in those bytes is non-zero. |
| 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 |
| } |