document renames and simplification
[epoint] / document / document.go
1 package document
2
3 // An epoint document is a clear signed utf-8 text of key-value pairs
4 // according to OpenPGP RFC 4880. The body contains a content-type
5 // MIME header so it can be used in OpenPGP/MIME RFC 3156 emails with
6 // the signature detached. The format of the key-value pairs are
7 // similar to MIME header fields.
8 //
9 // Example:
10 //
11 // -----BEGIN PGP SIGNED MESSAGE-----
12 // Hash: SHA1
13 //
14 // Content-Type: text/plain.epoint.cert; charset=utf-8
15 //
16 // Key: Value1
17 // Another-Key: Value2
18 // -----BEGIN PGP SIGNATURE-----
19 // pgp signature
20 // -----END PGP SIGNATURE-----
21
22 import (
23         "bytes"
24         "encoding/hex"
25         "fmt"
26         "reflect"
27         "strconv"
28         "strings"
29         "time"
30 )
31
32 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
33
34 // (non-standard) MIME type for epoint documents, see RFC 2045 and RFC 2046
35 var ContentType = map[string]string{
36         "cert":  "text/plain.epoint.cert; charset=utf-8",
37         "draft": "text/plain.epoint.draft; charset=utf-8",
38 }
39
40 // OpenPGP signed cleartext document representation
41 type ClearSigned struct {
42         Hash string
43         // Signed text (no dash escape, no trailing space)
44         Body []byte
45         // Armored detached text signature of the Body
46         ArmoredSignature []byte
47 }
48
49 type Field struct {
50         Key   string
51         Value string
52 }
53
54 // Draft document represents an obligation transfer order
55 type Draft struct {
56         Drawer       string `marshal:"id"`
57         Beneficiary  string `marshal:"id"`
58         Amount       int64
59         Denomination string
60         Issuer       string `marshal:"id"`
61         AuthorizedBy string `marshal:"id" key:"Authorized-By"`
62         Date         int64  `marshal:"date"`
63         MaturityDate int64  `marshal:"date" key:"Maturity-Date"`
64         ExpiryDate   int64  `marshal:"date" key:"Expiry-Date"`
65         Notes        string
66         Nonce        string
67         // useful if more strict date of issue information is needed
68         //References []string
69 }
70
71 // Obligation certificate after a transfer
72 // References previous certificate (if any)
73 // and the transfer related other documents
74 type Cert struct {
75         Holder           string `marshal:"id"`
76         Serial           int64
77         Date             int64 `marshal:"date"`
78         Balance          int64
79         Denomination     string
80         Issuer           string `marshal:"id"`
81         AuthorizedBy     string `marshal:"id" key:"Authorized-By"`
82         LastDebitSerial  int64  `key:"Last-Debit-Serial"`
83         LastCreditSerial int64  `key:"Last-Credit-Serial"`
84         LastCert         string `marshal:"id" key:"Last-Cert"`
85         Difference       int64
86         Draft            string   `marshal:"id"`
87         Drawer           string   `marshal:"id"`
88         DrawerCert       string   `marshal:"id" key:"Drawer-Cert"`
89         Notes            string   // TODO: server or drawer?
90         References       []string `marshal:"idlist"`
91 }
92
93 func ParseClearSigned(s []byte) (c *ClearSigned, err error) {
94         hash, body, sig := split(s)
95         if len(sig) == 0 {
96                 err = fmt.Errorf("ParseClearSigned could parse the signed document")
97                 return
98         }
99         c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
100         return
101 }
102
103 func FormatClearSigned(c *ClearSigned) (data []byte, err error) {
104         s := ClearSignedHeader
105         if c.Hash != "" {
106                 s += "Hash: " + c.Hash + "\n"
107         }
108         // TODO: check if space was trimmed from body before signature
109         s += "\n"
110         s += string(dashesc(c.Body))
111         s += "\n"
112         s += string(c.ArmoredSignature)
113         data = []byte(s)
114         return
115 }
116
117 func ParseFields(s []byte) (fields []Field, rest []byte, err error) {
118         rest = s
119         for len(rest) > 0 {
120                 var line []byte
121                 line, rest = getLine(rest)
122                 // empty line after the parsed fields (consumed)
123                 if len(line) == 0 {
124                         break
125                 }
126                 i := bytes.Index(line, []byte(": "))
127                 if i < 0 {
128                         err = fmt.Errorf("ParseFields: missing ': '\n")
129                         break
130                 }
131                 fields = append(fields, Field{string(line[:i]), string(line[i+2:])})
132         }
133         return
134 }
135
136 func ParseBody(s []byte) (doctype string, fields []Field, err error) {
137         // parse content type header first
138         fs, s, err := ParseFields(s)
139         if err != nil {
140                 return
141         }
142         if len(fs) != 1 || fs[0].Key != "Content-Type" {
143                 return "", nil, fmt.Errorf("ParseBody: single Content-Type header was expected\n")
144         }
145         ctype := fs[0].Value
146         for k, v := range ContentType {
147                 if ctype == v {
148                         doctype = k
149                         break
150                 }
151         }
152         if doctype == "" {
153                 return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
154         }
155         fields, s, err = ParseFields(s)
156         if err == nil && len(s) > 0 {
157                 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
158         }
159         return
160 }
161
162 func parse(s []byte, doctype string) (v interface{}, err error) {
163         t, fields, err := ParseBody(s)
164         if err != nil {
165                 return
166         }
167         if doctype != t && doctype != "" {
168                 err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t)
169                 return
170         }
171         switch t {
172         case "draft":
173                 v = new(Draft)
174         case "cert":
175                 v = new(Cert)
176         default:
177                 err = fmt.Errorf("parse: unkown doc type: %s", t)
178                 return
179         }
180         err = unmarshal(fields, v)
181         return
182 }
183
184 // TODO: limit errors
185 func format(v interface{}) ([]byte, error) {
186         doctype := ""
187         switch v.(type) {
188         case *Draft:
189                 doctype = "draft"
190         case *Cert:
191                 doctype = "cert"
192         default:
193                 panic("reder: unknown type")
194         }
195         s := "Content-Type: " + ContentType[doctype] + "\n\n"
196         fields := marshal(v)
197         for _, f := range fields {
198                 s += f.Key + ": " + f.Value + "\n"
199         }
200         return []byte(s), nil
201 }
202
203 func ParseDraft(s []byte) (draft *Draft, err error) {
204         v, err := parse(s, "draft")
205         if err != nil {
206                 return
207         }
208         draft = v.(*Draft)
209         return
210 }
211
212 func FormatDraft(draft *Draft) ([]byte, error) {
213         return format(draft)
214 }
215
216 func ParseCert(s []byte) (cert *Cert, err error) {
217         v, err := parse(s, "cert")
218         if err != nil {
219                 return nil, err
220         }
221         cert = v.(*Cert)
222         return
223 }
224
225 func FormatCert(cert *Cert) ([]byte, error) {
226         return format(cert)
227 }
228
229 func parseId(s string) (string, error) {
230         if len(s) != 40 {
231                 return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
232         }
233         dst := make([]byte, len(s)/2)
234         _, err := hex.Decode(dst, []byte(s))
235         return s, err
236 }
237
238 func formatId(s string) string {
239         return s
240 }
241
242 func parseString(s string) (string, error) {
243         if len(s) > 140 {
244                 return "", fmt.Errorf("parseString: 140 chars limit is exceeded")
245         }
246         return s, nil
247 }
248
249 func formatString(s string) string {
250         return s
251 }
252
253 func parseDate(s string) (int64, error) {
254         t, err := time.Parse(time.RFC3339, s)
255         if err != nil {
256                 return 0, err
257         }
258         return t.Seconds(), nil
259 }
260
261 func formatDate(i int64) string {
262         return time.SecondsToUTC(i).Format(time.RFC3339)
263 }
264
265 func unmarshal(fields []Field, iv interface{}) error {
266         v := reflect.ValueOf(iv)
267         if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
268                 panic("unmarshal: input is not a pointer to struct")
269         }
270         v = v.Elem()
271         t := v.Type()
272         n := v.NumField()
273         if len(fields) != n {
274                 return fmt.Errorf("unmarshal: %s has %d fields, got %d\n", t.Name(), n, len(fields))
275         }
276         for i := 0; i < n; i++ {
277                 ft := t.Field(i)
278                 fv := v.Field(i)
279                 m := ft.Tag.Get("marshal")
280                 k := ft.Tag.Get("key")
281                 if k == "" {
282                         k = ft.Name
283                 }
284                 if fields[i].Key != k {
285                         return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i+1, t.Name(), k)
286                 }
287                 s := fields[i].Value
288                 var err error
289                 switch fv.Kind() {
290                 case reflect.String:
291                         var val string
292                         switch m {
293                         case "id":
294                                 val, err = parseId(s)
295                         case "":
296                                 val, err = parseString(s)
297                         default:
298                                 panic("bad string field tag")
299                         }
300                         fv.SetString(val)
301                 case reflect.Int, reflect.Int32, reflect.Int64:
302                         var val int64
303                         switch m {
304                         case "date":
305                                 val, err = parseDate(s)
306                         case "":
307                                 val, err = strconv.Atoi64(s)
308                         default:
309                                 panic("bad int field tag")
310                         }
311                         fv.SetInt(val)
312                 case reflect.Uint, reflect.Uint32, reflect.Uint64:
313                         var val uint64
314                         switch m {
315                         case "":
316                                 val, err = strconv.Atoui64(s)
317                         default:
318                                 panic("bad uint field tag")
319                         }
320                         fv.SetUint(val)
321                 case reflect.Slice:
322                         var val []string
323                         switch m {
324                         case "idlist":
325                                 if fv.Type().Elem().Kind() != reflect.String {
326                                         panic("only string slice is supported")
327                                 }
328                                 ids := strings.Split(s, " ")
329                                 val = make([]string, len(ids))
330                                 for j := range val {
331                                         val[j], err = parseId(ids[j])
332                                         if err != nil {
333                                                 return err
334                                         }
335                                 }
336                         default:
337                                 panic("bad slice field tag")
338                         }
339                         fv.Set(reflect.ValueOf(val))
340                 default:
341                         panic("bad field type")
342                 }
343                 if err != nil {
344                         return err
345                 }
346         }
347         return nil
348 }
349
350 func marshal(iv interface{}) []Field {
351         v := reflect.ValueOf(iv)
352         if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
353                 panic("unmarshal: input is not a pointer to struct")
354         }
355         v = v.Elem()
356         t := v.Type()
357         n := v.NumField()
358         fields := []Field{}
359         for i := 0; i < n; i++ {
360                 ft := t.Field(i)
361                 fv := v.Field(i)
362                 m := ft.Tag.Get("marshal")
363                 k := ft.Tag.Get("key")
364                 if k == "" {
365                         k = ft.Name
366                 }
367                 val := ""
368                 switch fv.Kind() {
369                 case reflect.String:
370                         switch m {
371                         case "id":
372                                 val = formatId(fv.String())
373                         case "":
374                                 val = formatString(fv.String())
375                         default:
376                                 panic("bad string field tag")
377                         }
378                 case reflect.Int, reflect.Int32, reflect.Int64:
379                         switch m {
380                         case "date":
381                                 val = formatDate(fv.Int())
382                         case "":
383                                 val = strconv.Itoa64(fv.Int())
384                         default:
385                                 panic("bad int field tag")
386                         }
387                 case reflect.Uint, reflect.Uint32, reflect.Uint64:
388                         switch m {
389                         case "":
390                                 val = strconv.Uitoa64(fv.Uint())
391                         default:
392                                 panic("bad uint field tag")
393                         }
394                 case reflect.Slice:
395                         switch m {
396                         case "idlist":
397                                 if fv.Type().Elem().Kind() != reflect.String {
398                                         panic("only string slice is supported")
399                                 }
400                                 k := fv.Len()
401                                 for j := 0; j < k; j++ {
402                                         if j > 0 {
403                                                 val += " "
404                                         }
405                                         val += formatId(fv.Index(j).String())
406                                 }
407                         default:
408                                 panic("bad slice field tag")
409                         }
410                 default:
411                         panic("bad field type")
412                 }
413                 fields = append(fields, Field{k, val})
414         }
415         return fields
416 }
417
418 func getLine(data []byte) (line, rest []byte) {
419         i := bytes.Index(data, []byte{'\n'})
420         j := i + 1
421         if i < 0 {
422                 i = len(data)
423                 j = i
424         } else if i > 0 && data[i-1] == '\r' {
425                 i--
426         }
427         return data[:i], data[j:]
428 }
429
430 func trimspace(s []byte) []byte {
431         a := bytes.Split(s, []byte("\n"))
432         for i := range a {
433                 a[i] = bytes.TrimRight(a[i], " \t\r")
434         }
435         return bytes.Join(a, []byte("\n"))
436 }
437
438 func dashesc(s []byte) []byte {
439         r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1)
440         if len(r) > 0 && r[0] == '-' {
441                 r = append([]byte("- "), r...)
442         }
443         return r
444 }
445
446 func dashunesc(s []byte) []byte {
447         r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1)
448         if len(r) >= 2 && r[0] == '-' && r[1] == ' ' {
449                 r = r[2:]
450         }
451         return r
452 }
453
454 // RFC 4880 is unclear about multiple Hash header semantics, section 7. says
455 //  "One or more "Hash" Armor Headers"
456 // then
457 //  "If more than one message digest is used in the signature, the "Hash"
458 //   armor header contains a comma-delimited list of used message digests."
459 // in section 6.2.
460 //  "there is no limit to the length of Armor Headers.  Care should
461 //   be taken that the Armor Headers are short enough to survive
462 //   transport.  One way to do this is to repeat an Armor Header key
463 //   multiple times with different values for each so that no one line is
464 //   overly long."
465 // we accept a single Hash header with a list of hash algorithms for now
466 // but use the one specified by the signature
467
468 func split(s []byte) (hash, body, sig []byte) {
469         if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
470                 return
471         }
472         s = s[len(ClearSignedHeader):]
473         // only allow a single Hash: header
474         if bytes.HasPrefix(s, []byte("Hash: ")) {
475                 s = s[len("Hash: "):]
476                 hash, s = getLine(s)
477         }
478         // skip empty line
479         empty, s := getLine(s)
480         if len(empty) != 0 {
481                 return
482         }
483         i := bytes.Index(s, []byte("\n-----BEGIN"))
484         if i < 0 {
485                 return
486         }
487         body, sig = s[:i], s[i+1:]
488         if i > 0 && body[i-1] == '\r' {
489                 body = body[:i-1]
490         }
491         return
492 }