]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - passwd.go
README: minor updates and fixes
[nsscash/nsscash.git] / passwd.go
1 // Parse /etc/passwd files and serialize them
2
3 // Copyright (C) 2019  Simon Ruderich
4 //
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU Affero General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU Affero General Public License for more details.
14 //
15 // You should have received a copy of the GNU Affero General Public License
16 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
18 package main
19
20 import (
21         "bufio"
22         "bytes"
23         "encoding/binary"
24         "fmt"
25         "io"
26         "sort"
27         "strconv"
28         "strings"
29
30         "github.com/pkg/errors"
31 )
32
33 // Version written in SerializePasswds()
34 const PasswdVersion = 1
35
36 type Passwd struct {
37         Name   string
38         Passwd string
39         Uid    uint64
40         Gid    uint64
41         Gecos  string
42         Dir    string
43         Shell  string
44 }
45
46 // ParsePasswds parses a file in the format of /etc/passwd and returns all
47 // entries as Passwd structs.
48 func ParsePasswds(r io.Reader) ([]Passwd, error) {
49         var res []Passwd
50
51         s := bufio.NewScanner(r)
52         for s.Scan() {
53                 t := s.Text()
54
55                 x := strings.Split(t, ":")
56                 if len(x) != 7 {
57                         return nil, fmt.Errorf("invalid line %q", t)
58                 }
59
60                 uid, err := strconv.ParseUint(x[2], 10, 64)
61                 if err != nil {
62                         return nil, errors.Wrapf(err, "invalid uid in line %q", t)
63                 }
64                 gid, err := strconv.ParseUint(x[3], 10, 64)
65                 if err != nil {
66                         return nil, errors.Wrapf(err, "invalid gid in line %q", t)
67                 }
68
69                 res = append(res, Passwd{
70                         Name:   x[0],
71                         Passwd: x[1],
72                         Uid:    uid,
73                         Gid:    gid,
74                         Gecos:  x[4],
75                         Dir:    x[5],
76                         Shell:  x[6],
77                 })
78         }
79         err := s.Err()
80         if err != nil {
81                 return nil, err
82         }
83
84         return res, nil
85 }
86
87 func SerializePasswd(p Passwd) []byte {
88         // Concatenate all (NUL-terminated) strings and store the offsets
89         var data bytes.Buffer
90         data.Write([]byte(p.Name))
91         data.WriteByte(0)
92         offPasswd := uint16(data.Len())
93         data.Write([]byte(p.Passwd))
94         data.WriteByte(0)
95         offGecos := uint16(data.Len())
96         data.Write([]byte(p.Gecos))
97         data.WriteByte(0)
98         offDir := uint16(data.Len())
99         data.Write([]byte(p.Dir))
100         data.WriteByte(0)
101         offShell := uint16(data.Len())
102         data.Write([]byte(p.Shell))
103         data.WriteByte(0)
104         size := uint16(data.Len())
105
106         var res bytes.Buffer // serialized result
107         le := binary.LittleEndian
108
109         id := make([]byte, 8)
110         // uid
111         le.PutUint64(id, p.Uid)
112         res.Write(id)
113         // gid
114         le.PutUint64(id, p.Gid)
115         res.Write(id)
116
117         off := make([]byte, 2)
118         // off_passwd
119         le.PutUint16(off, offPasswd)
120         res.Write(off)
121         // off_gecos
122         le.PutUint16(off, offGecos)
123         res.Write(off)
124         // off_dir
125         le.PutUint16(off, offDir)
126         res.Write(off)
127         // off_shell
128         le.PutUint16(off, offShell)
129         res.Write(off)
130         // data_size
131         le.PutUint16(off, size)
132         res.Write(off)
133
134         res.Write(data.Bytes())
135         // We must pad each entry so that all uint64 at the beginning of the
136         // struct are 8 byte aligned
137         l := res.Len()
138         if l%8 != 0 {
139                 for i := 0; i < 8-l%8; i++ {
140                         res.WriteByte(0)
141                 }
142         }
143
144         return res.Bytes()
145 }
146
147 func SerializePasswds(w io.Writer, pws []Passwd) error {
148         // Serialize passwords and store offsets
149         var data bytes.Buffer
150         offsets := make(map[Passwd]uint64)
151         for _, p := range pws {
152                 // TODO: warn about duplicate entries
153                 offsets[p] = uint64(data.Len())
154                 data.Write(SerializePasswd(p))
155         }
156
157         // Copy to prevent sorting from modifying the argument
158         sorted := make([]Passwd, len(pws))
159         copy(sorted, pws)
160
161         le := binary.LittleEndian
162         tmp := make([]byte, 8)
163
164         // Create index "sorted" in input order, used when iterating over all
165         // passwd entries (getpwent_r); keeping the original order makes
166         // debugging easier
167         var indexOrig bytes.Buffer
168         for _, p := range pws {
169                 le.PutUint64(tmp, offsets[p])
170                 indexOrig.Write(tmp)
171         }
172
173         // Create index sorted after id
174         var indexId bytes.Buffer
175         sort.Slice(sorted, func(i, j int) bool {
176                 return sorted[i].Uid < sorted[j].Uid
177         })
178         for _, p := range sorted {
179                 le.PutUint64(tmp, offsets[p])
180                 indexId.Write(tmp)
181         }
182
183         // Create index sorted after name
184         var indexName bytes.Buffer
185         sort.Slice(sorted, func(i, j int) bool {
186                 return sorted[i].Name < sorted[j].Name
187         })
188         for _, p := range sorted {
189                 le.PutUint64(tmp, offsets[p])
190                 indexName.Write(tmp)
191         }
192
193         // Sanity check
194         if len(pws)*8 != indexOrig.Len() ||
195                 indexOrig.Len() != indexId.Len() ||
196                 indexId.Len() != indexName.Len() {
197                 return fmt.Errorf("indexes have inconsistent length")
198         }
199
200         // Write result
201
202         // magic
203         w.Write([]byte("NSS-CASH"))
204         // version
205         le.PutUint64(tmp, PasswdVersion)
206         w.Write(tmp)
207         // count
208         le.PutUint64(tmp, uint64(len(pws)))
209         w.Write(tmp)
210         // off_orig_index
211         offset := uint64(0)
212         le.PutUint64(tmp, offset)
213         w.Write(tmp)
214         // off_id_index
215         offset += uint64(indexOrig.Len())
216         le.PutUint64(tmp, offset)
217         w.Write(tmp)
218         // off_name_index
219         offset += uint64(indexId.Len())
220         le.PutUint64(tmp, offset)
221         w.Write(tmp)
222         // off_data
223         offset += uint64(indexName.Len())
224         le.PutUint64(tmp, offset)
225         w.Write(tmp)
226
227         _, err := indexOrig.WriteTo(w)
228         if err != nil {
229                 return err
230         }
231         _, err = indexId.WriteTo(w)
232         if err != nil {
233                 return err
234         }
235         _, err = indexName.WriteTo(w)
236         if err != nil {
237                 return err
238         }
239         _, err = data.WriteTo(w)
240         if err != nil {
241                 return err
242         }
243
244         return nil
245 }