+package document
+
+// An epoint document is a clear signed utf-8 text of key-value pairs
+// according to OpenPGP RFC 4880. The body contains a content-type
+// MIME header so it can be used in OpenPGP/MIME RFC 3156 emails with
+// the signature detached. The format of the key-value pairs are
+// similar to MIME header fields.
+//
+// TODO: allow visually aligned field values
+// TODO: handling repeated fields
+// TODO: representation of a list (references)
+//
+// Example:
+//
+// -----BEGIN PGP SIGNED MESSAGE-----
+// Hash: SHA1
+//
+// Content-Type: text/plain.epoint.cert; charset=utf-8
+//
+// Key: Value1
+// Another-Key: Value2
+// -----BEGIN PGP SIGNATURE-----
+// pgp signature
+// -----END PGP SIGNATURE-----
+
+import (
+ "bytes"
+ "strconv"
+)
+
+const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
+
+// Non-standard MIME subtype see RFC 2045 and RFC 2046
+// TODO: verify that Content-Transfer-Encoding is not needed
+// TODO: text/epoint.cert would be shorter
+var ContentType = map[string]string{
+ "cert": "text/plain.epoint.cert; charset=utf-8",
+ "draft": "text/plain.epoint.draft; charset=utf-8",
+}
+
+// OpenPGP signed cleartext document representation
+type ClearSigned struct {
+ Hash string
+ // Signed text (no dash escape, no trailing space)
+ Body []byte
+ // Armored detached text signature of the Body
+ ArmoredSignature []byte
+}
+
+// Draft document represents an obligation transfer order
+type Draft struct {
+ Drawer string // ID of the payer (signer of the document)
+ Beneficiary string // ID of the payee
+ Amount int64 // amount transfered
+ // TODO: issuer keys is enough?
+ Denomination string
+ // TODO: maturity date is enough if the only constraint is <= now
+ IssueDate int64
+ MaturityDate int64 // Draft is bounced before this date
+ // TODO: implement limits
+ Notes string // Arbitrary text notes of the drawer
+ // TODO: hack to make signed draft unique (not required for DSA)
+ Nonce string // unique number
+ // TODO: server ID might change, do we need it?
+ Server string // ID of the server (drawee?)
+ //TODO: naming: drawee vs issuer
+ Drawee string // ID of the obligation issuer
+ // TODO: reference cert ids in the draft
+ // useful if more strict date of issue information is needed
+ //References []string
+}
+
+// TODO: cert references: fpr+serial, fpr+certID
+// Certification of obligation after a transfer
+// References previous certificate (if any)
+// and the transfer related other documents
+type Cert struct {
+ Holder string // ID of the creditor
+ Serial uint32 // serial number, number of certs of the holder
+ Date int64 // date of issue
+ Balance int64 // current obligation value
+ Denomination string
+ Issuer string // ID of the obligation issuer (drawee?)
+ LastDebitSerial uint32 // serial of the last draft cert or 0
+ LastCreditSerial uint32 // serial of the last credit cert or 0
+ // TODO: move to References?
+ LastCert string // ID of the previous cert if any
+ // TODO: determine cert type from diff value only?
+ // (>0: credit cert, <0: debit cert, ==0: special)
+ Difference int64 // difference from previous balance
+ // TODO: enough on the debit side
+ Draft string // draft ID related to the transfer
+ // TODO: credit side, redundant references
+ Drawer string // ID of the drawer in the transaction
+ DrawerSerial uint32 // serial of the drawer's related debit cert
+ DrawerCert string // ID of the drawer's related debit cert
+ // TODO: fingerprint?
+ References []string // cert IDs for timestamping the system
+}
+
+func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
+ hash, body, sig := split(s)
+ if len(sig) == 0 {
+ // TODO: split errors
+ return
+ }
+ c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
+ return
+}
+
+func EncodeClearSigned(c *ClearSigned) (data []byte, err error) {
+ s := ClearSignedHeader
+ if c.Hash != "" {
+ s += "Hash: " + c.Hash + "\n"
+ }
+ // TODO: check if space was trimmed from body before signature
+ s += "\n"
+ s += string(dashesc(c.Body))
+ s += "\n"
+ s += string(c.ArmoredSignature)
+ data = []byte(s)
+ return
+}
+
+func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
+ // TODO: error
+ fields = make(map[string]string)
+ rest = s
+ for len(rest) > 0 {
+ var line []byte
+ line, rest = getLine(rest)
+ // empty line after the parsed fields (consumed)
+ if len(line) == 0 {
+ break
+ }
+ i := bytes.Index(line, []byte(": "))
+ // TODO: long lines can be broken up in MIME
+ if i < 0 {
+ return nil, nil, nil
+ }
+ // TODO: repeated fields
+ fields[string(line[:i])] = string(line[i+2:])
+ }
+ return
+}
+
+func ParseBody(s []byte) (t string, fields map[string]string, err error) {
+ // parse content type header first
+ mime, s, err := ParseFields(s)
+ if len(mime) != 1 {
+ return
+ }
+ for k, v := range ContentType {
+ if mime["Content-Type"] == v {
+ t = k
+ fields, s, err = ParseFields(s)
+ if len(s) > 0 {
+ fields = nil
+ break
+ }
+ return
+ }
+ }
+ // TODO: error
+ return
+}
+
+/* rendering with reflect
+func render(d interface{}) (s []byte, err error) {
+ a := []string{}
+ v := reflect.ValueOf(d)
+ t := v.Type()
+ n := v.NumField()
+ for i := 0; i < n; i++ {
+ f := t.Field(i)
+ fv := v.Field(i)
+ fs := ""
+ switch fv.Type() {
+ case reflect.String:
+ fs = fv.String() // TODO: quote, esc (\n..)
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ fs = strconv.Itoa64(fv.Int())
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ fs = strconv.Uitoa64(fv.Uint())
+ default:
+ return // TODO: error
+ }
+ a = append(a, f.Name, ": ", fs, "\n")
+ }
+ s = strings.Join(a, "")
+ return
+}
+*/
+
+func checkID(s string) (string, error) {
+ return s, nil
+}
+
+func ParseDraft(s []byte) (draft *Draft, err error) {
+ t, fields, err := ParseBody(s)
+ if err != nil {
+ return
+ }
+ if t != "draft" {
+ return
+ }
+
+ draftFields := []string{
+ "Drawer",
+ "Beneficiary",
+ "Amount",
+ "Denomination",
+ "IssueDate",
+ "MaturityDate",
+ "Notes",
+ "Nonce",
+ "Server",
+ "Drawee",
+ }
+ if len(fields) != len(draftFields) {
+ return
+ }
+ for _, f := range draftFields {
+ _, ok := fields[f]
+ if !ok {
+ return
+ }
+ }
+
+ draft = new(Draft)
+ draft.Drawer, _ = checkID(fields["Drawer"])
+ draft.Beneficiary, _ = checkID(fields["Beneficiary"])
+ draft.Amount, _ = strconv.Atoi64(fields["Amount"])
+ draft.Denomination = fields["Denomination"]
+ draft.IssueDate, _ = strconv.Atoi64(fields["IssueDate"])
+ draft.MaturityDate, _ = strconv.Atoi64(fields["MaturityDate"])
+ draft.Notes = fields["Notes"]
+ draft.Nonce = fields["Nonce"]
+ draft.Server, _ = checkID(fields["Server"])
+ draft.Drawee, _ = checkID(fields["Drawee"])
+
+ // more checks..
+
+ return
+}
+
+func RenderDraft(draft *Draft) (data []byte, err error) {
+ s := "Content-Type: " + ContentType["draft"] + "\n"
+ s += "\n"
+ s += "Drawer: " + draft.Drawer + "\n"
+ s += "Beneficiary: " + draft.Beneficiary + "\n"
+ s += "Amount: " + strconv.Itoa64(draft.Amount) + "\n"
+ s += "Denomination: " + draft.Denomination + "\n"
+ s += "IssueDate: " + strconv.Itoa64(draft.IssueDate) + "\n"
+ s += "MaturityDate: " + strconv.Itoa64(draft.MaturityDate) + "\n"
+ s += "Notes: " + draft.Notes + "\n"
+ s += "Nonce: " + draft.Nonce + "\n"
+ s += "Server: " + draft.Server + "\n"
+ s += "Drawee: " + draft.Drawee + "\n"
+ data = []byte(s)
+ return
+}
+
+func ParseCert(s []byte) (cert Cert, err error) {
+ return
+}
+
+func RenderCert(cert Cert) (s []byte, err error) {
+ return
+}
+
+func splitline(s []byte) (line, rest []byte) {
+ i := bytes.IndexByte(s, '\n')
+ if i < 0 {
+ line = s
+ } else {
+ rest = s[i+1:]
+ if i > 0 && s[i-1] == '\r' {
+ i--
+ }
+ line = s[:i]
+ }
+ return
+}
+
+func getLine(data []byte) (line, rest []byte) {
+ i := bytes.Index(data, []byte{'\n'})
+ j := i + 1
+ if i < 0 {
+ i = len(data)
+ j = i
+ } else if i > 0 && data[i-1] == '\r' {
+ i--
+ }
+ return data[:i], data[j:]
+}
+
+func trimspace(s []byte) []byte {
+ a := bytes.Split(s, []byte("\n"))
+ for i := range a {
+ a[i] = bytes.TrimRight(a[i], " \t\r")
+ }
+ return bytes.Join(a, []byte("\n"))
+}
+
+func dashesc(s []byte) []byte {
+ r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1)
+ if len(r) > 0 && r[0] == '-' {
+ r = append([]byte("- "), r...)
+ }
+ return r
+}
+
+func dashunesc(s []byte) []byte {
+ r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1)
+ if len(r) >= 2 && r[0] == '-' && r[1] == ' ' {
+ r = r[2:]
+ }
+ return r
+}
+
+// RFC 4880 is unclear about multiple Hash header semantics, section 7. says
+// "One or more "Hash" Armor Headers"
+// then
+// "If more than one message digest is used in the signature, the "Hash"
+// armor header contains a comma-delimited list of used message digests."
+// in section 6.2.
+// "there is no limit to the length of Armor Headers. Care should
+// be taken that the Armor Headers are short enough to survive
+// transport. One way to do this is to repeat an Armor Header key
+// multiple times with different values for each so that no one line is
+// overly long."
+// we accept a single Hash header with a list of hash algorithms for now
+// but use the one specified by the signature
+
+func split(s []byte) (hash, body, sig []byte) {
+ if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
+ return
+ }
+ s = s[len(ClearSignedHeader):]
+ // only allow a single Hash: header
+ if bytes.HasPrefix(s, []byte("Hash: ")) {
+ s = s[len("Hash: "):]
+ hash, s = getLine(s)
+ }
+ // skip empty line
+ empty, s := getLine(s)
+ if len(empty) != 0 {
+ return
+ }
+ i := bytes.Index(s, []byte("\n-----BEGIN"))
+ if i < 0 {
+ return
+ }
+ body, sig = s[:i], s[i+1:]
+ if i > 0 && body[i-1] == '\r' {
+ body = body[:i-1]
+ }
+ return
+}