From 8cbebeac7b4e3d4cf4ac0358ecfbf64f72732429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Fr=C3=B6hle?= Date: Tue, 20 Feb 2018 03:57:21 +0100 Subject: [PATCH] Initial version - stable v0.1.0 --- .gitignore | 2 + fixtures/paypal.pem | 22 ++++ fixtures/test.html | 15 +++ paypal.go | 315 ++++++++++++++++++++++++++++++++++++++++++++ paypal_test.go | 25 ++++ 5 files changed, 379 insertions(+) create mode 100644 fixtures/paypal.pem create mode 100644 fixtures/test.html create mode 100644 paypal.go create mode 100644 paypal_test.go diff --git a/.gitignore b/.gitignore index a1338d6..f479089 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +.idea/ \ No newline at end of file diff --git a/fixtures/paypal.pem b/fixtures/paypal.pem new file mode 100644 index 0000000..4639da3 --- /dev/null +++ b/fixtures/paypal.pem @@ -0,0 +1,22 @@ +live_api +-----BEGIN CERTIFICATE----- +MIIDgzCCAuygAwIBAgIBADANBgkqhkiG9w0BAQUFADCBjjELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQ +YXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9h +cGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20wHhcNMDQwMjEzMTAxMzE1 +WhcNMzUwMjEzMTAxMzE1WjCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYw +FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEG +A1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0B +CQEWDXJlQHBheXBhbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMFH +Tt38RMxLXJyO2SmS+Ndl72T7oKJ4u4uw+6awntALWh03PewmIJuzbALScsTS4sZo +S1fKciBGoh11gIfHzylvkdNe/hJl66/RGqrj5rFb08sAABNTzDTiqqNpJeBsYs/c +2aiGozptX2RlnBktH+SUNpAajW724Nv2Wvhif6sFAgMBAAGjge4wgeswHQYDVR0O +BBYEFJaffLvGbxe9WT9S1wob7BDWZJRrMIG7BgNVHSMEgbMwgbCAFJaffLvGbxe9 +WT9S1wob7BDWZJRroYGUpIGRMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMw +EQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3 +DQEJARYNcmVAcGF5cGFsLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB +BQUAA4GBAIFfOlaagFrl71+jq6OKidbWFSE+Q4FqROvdgIONth+8kSK//Y/4ihuE +4Ymvzn5ceE3S/iBSQQMjyvb+s2TWbQYDwcp129OPIbD9epdr4tJOUNiSojw7BHwY +RiPh58S1xGlFgHFXwrEBb3dgNbMUa+u4qectsMAXpVHnD9wIyfmH +-----END CERTIFICATE----- diff --git a/fixtures/test.html b/fixtures/test.html new file mode 100644 index 0000000..1089ae8 --- /dev/null +++ b/fixtures/test.html @@ -0,0 +1,15 @@ + + + + + Title + + +
+ + + + +
+ + \ No newline at end of file diff --git a/paypal.go b/paypal.go new file mode 100644 index 0000000..2486a95 --- /dev/null +++ b/paypal.go @@ -0,0 +1,315 @@ +package Ewp + +import ( + "crypto/x509" + "crypto/rsa" + "encoding/pem" + "encoding/base64" + "errors" + "io/ioutil" + "reflect" + "strings" + "fmt" + + "github.com/fullsailor/pkcs7" + "strconv" +) + +const tagName = "ppewp" + +const ppCertPEM = `live_api +-----BEGIN CERTIFICATE----- +MIIDgzCCAuygAwIBAgIBADANBgkqhkiG9w0BAQUFADCBjjELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQ +YXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9h +cGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20wHhcNMDQwMjEzMTAxMzE1 +WhcNMzUwMjEzMTAxMzE1WjCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYw +FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEG +A1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0B +CQEWDXJlQHBheXBhbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMFH +Tt38RMxLXJyO2SmS+Ndl72T7oKJ4u4uw+6awntALWh03PewmIJuzbALScsTS4sZo +S1fKciBGoh11gIfHzylvkdNe/hJl66/RGqrj5rFb08sAABNTzDTiqqNpJeBsYs/c +2aiGozptX2RlnBktH+SUNpAajW724Nv2Wvhif6sFAgMBAAGjge4wgeswHQYDVR0O +BBYEFJaffLvGbxe9WT9S1wob7BDWZJRrMIG7BgNVHSMEgbMwgbCAFJaffLvGbxe9 +WT9S1wob7BDWZJRroYGUpIGRMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMw +EQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3 +DQEJARYNcmVAcGF5cGFsLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB +BQUAA4GBAIFfOlaagFrl71+jq6OKidbWFSE+Q4FqROvdgIONth+8kSK//Y/4ihuE +4Ymvzn5ceE3S/iBSQQMjyvb+s2TWbQYDwcp129OPIbD9epdr4tJOUNiSojw7BHwY +RiPh58S1xGlFgHFXwrEBb3dgNbMUa+u4qectsMAXpVHnD9wIyfmH +-----END CERTIFICATE-----` + +var ppCert *x509.Certificate + +func init() { + block, _ := pem.Decode([]byte(ppCertPEM)) + if block == nil { + panic("failed to parse certificate PEM") + } + var err error + ppCert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + panic("failed to parse certificate: " + err.Error()) + } + +} + +type CryptData struct { + Cmd string `ppewp:"name:cmd;type:string;default:_xclick"` // cmd = '_xclick', + Business string `ppewp:"name:business;type:string"` // business = 'mein@paypalaccount.de', + ItemName string `ppewp:"name:item_name;type:string"` //Test Gegenstand', // Name der Bestellung + ItemNumber string `ppewp:"name:item_number;type:string"` // OderID or ItemID + Amount float64 `ppewp:"name:amount;type:float64"` //00.01', // Wert + ReturnUrl string `ppewp:"name:return;type:string;omitempty"` //http://www.beispiel.de/paypal_ok.php', // URL fuer erfolgreiche Zahlung + ReturnUrlCancel string `ppewp:"name:cancel_return;type:string;omitempty"` //http://www.beispiel.de/paypal_cancel.php', // URL fuer Zahlungsabbruch + NotifyUrl string `ppewp:"name:notify_url;type:string;omitempty"` //', + NoNote int `ppewp:"name:no_note;type:string;default:0"` //1', // Keine Notizen vom Kauefer moeglich + NoShipping int `ppewp:"name:no_shipping;type:string;default:0"` //1', + CurrencyCode string `ppewp:"name:currency_code;type:string;default:USD"` //EUR', + Lc string `ppewp:"name:lc;type:string;omitempty"` //DE', + Rm int `ppewp:"name:rm;type:string;default:0"` //2', // Der return-URL werden die Paramater als POST uebergeben + Bn string `ppewp:"name:bn;type:string;default:PP-BuyNowPP"` //PP-BuyNowBF', + Custom string `ppewp:"name:custom;type:string;omitempty"` //Irgendwas was mitgeschickt werden soll', + Invoice string `ppewp:"name:invoice;type:string;omitempty"` //', + ImageUrl string `ppewp:"name:image_url;type:string;omitempty"` //', + CppLogoImage string `ppewp:"name:cpp_logo_image;type:string;omitempty"` //', +} + +type EwpOptions struct { + Certificate string // Certificate resource + PrivateKey string // Private key resource (matching certificate) + PrivateKeyPassphrase string // Passphrase for the Private key resource (matching certificate) + PaypalCertificateFile string // Path to PayPal public certificate file - if need - the current of date 2018-02-18 is included + CertificateID string // ID assigned by PayPal to the $certificate. +} + +type Ewp struct { + certificate *x509.Certificate // Certificate resource + privateKey *rsa.PrivateKey // Private key resource (matching certificate) + certificateID *string // ID assigned by PayPal to the $certificate. + error error // error messages +} + +func NewPaypalEwp(options EwpOptions) *Ewp { + var ewp = &Ewp{} + ewp.LoadKeyPair(options) + + return ewp +} + +func (pe *Ewp) GetError() error { + return pe.error +} + +func (pe *Ewp) LoadKeyPair(options EwpOptions) { + pe.certificateID = &options.CertificateID + + if options.PaypalCertificateFile != "" { + certPEM, err := ioutil.ReadFile(options.PaypalCertificateFile) + if err != nil { + pe.error = err + return + } + block, _ := pem.Decode([]byte(certPEM)) + if block == nil { + panic("failed to parse certificate PEM") + } + ppCert, pe.error = x509.ParseCertificate(block.Bytes) + if pe.error != nil { + return + } + } + + certPEM, err := ioutil.ReadFile(options.Certificate) + if err != nil { + pe.error = err + return + } + block, _ := pem.Decode([]byte(certPEM)) + if block == nil { + panic("failed to parse certificate PEM") + } + pe.certificate, pe.error = x509.ParseCertificate(block.Bytes) + if pe.error != nil { + return + } + + keyFilePEM, err := ioutil.ReadFile(options.PrivateKey) + if err != nil { + pe.error = err + return + } + var keyPasswd []byte + if options.PrivateKeyPassphrase != "" { + keyPasswd = []byte(options.PrivateKeyPassphrase) + } + + pe.privateKey, pe.error = ParseRsaPrivateKeyFromPemStr(keyFilePEM, &keyPasswd) +} + +func (pe *Ewp) Generate(data *CryptData) string { + var encData []string + var output []byte + + encData = append(encData, "cert_id=" + *pe.certificateID) + + rt := reflect.TypeOf(data) + // Check if it's a pointer + if rt.Kind() != reflect.Ptr { + + pe.error = errors.New("It's not a pointer!") + return "" + } + + elField := rt.Elem() + + // Check if it's a struct + if elField.Kind() != reflect.Struct { + pe.error = errors.New("it's not a struct!") + return "" + } + + for i := 0; i < elField.NumField(); i++ { + field := elField.Field(i) + // value := refValue.Field(i) + kind := field.Type.Kind() + tagVals := parseTagSetting(field.Tag) + + var s string + switch kind { + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s = strconv.FormatInt(reflect.ValueOf(data).Elem().Field(i).Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + s = strconv.FormatUint(reflect.ValueOf(data).Elem().Field(i).Uint(), 10) + case reflect.Float32, reflect.Float64: + s = strconv.FormatFloat(reflect.ValueOf(data).Elem().Field(i).Float(), 'f', 2, 64) + case reflect.Bool: + if reflect.ValueOf(data).Elem().Field(i).Bool() { + s = "True" + } else { + s = "False" + } + case reflect.String: + s = reflect.ValueOf(data).Elem().Field(i).String() + } + if _, ok := tagVals["OMITEMPTY"]; ok && s == "" { + continue + } + + if s == "" { + s = tagVals["DEFAULT"]; + } + + encData = append(encData, tagVals["NAME"]+"="+s) + + fmt.Printf("%s (%v) = %#v >> %#v\n", reflect.ValueOf(data).Elem().Field(i).Type().Name(), kind, tagVals, s) + + } + + encFilled := strings.Join(encData, "\n") + + fmt.Printf("%#v\n\n", encFilled) + + signedData, err := pkcs7.NewSignedData([]byte(encFilled)) + if err != nil { + pe.error = err + return "" + } + + err = signedData.AddSigner(pe.certificate, pe.privateKey, pkcs7.SignerInfoConfig{}) + if err != nil { + pe.error = err + return "" + } + + //signedData.Detach() + + byteFinish, err := signedData.Finish() + if err != nil { + pe.error = err + return "" + } + + var cryptCerts []*x509.Certificate + cryptCerts = append(cryptCerts, ppCert) + + output, pe.error = pkcs7.Encrypt( + byteFinish, + cryptCerts, + ) + return "-----BEGIN PKCS7-----\n" + chunkSplit(base64.StdEncoding.EncodeToString(output), 64, "\n") + "-----END PKCS7-----" +} + +func ParseRsaPrivateKeyFromPemStr(privatePEM []byte, passphrase *[]byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(privatePEM) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the key") + } + + if x509.IsEncryptedPEMBlock(block) { + x509.DecryptPEMBlock(block, *passphrase) + block, _ = pem.Decode(privatePEM) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the secured key") + } + } + + privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + + return privateKey, nil +} + +func parseTagSetting(tags reflect.StructTag) map[string]string { + setting := map[string]string{} + for _, str := range []string{tags.Get(tagName)} { + tags := strings.Split(str, ";") + for _, value := range tags { + v := strings.Split(value, ":") + k := strings.TrimSpace(strings.ToUpper(v[0])) + if len(v) >= 2 { + setting[k] = strings.Join(v[1:], ":") + } else { + setting[k] = k + } + } + } + return setting +} + +func chunkSplit(body string, limit int, end string) string { + + var charSlice []rune + + // push characters to slice + for _, char := range body { + charSlice = append(charSlice, char) + } + + var result string = "" + + for len(charSlice) >= 1 { + // convert slice/array back to string + // but insert end at specified limit + + result = result + string(charSlice[:limit]) + end + + // discard the elements that were copied over to result + charSlice = charSlice[limit:] + + // change the limit + // to cater for the last few words in + // charSlice + if len(charSlice) < limit { + limit = len(charSlice) + } + + } + + return result + +} diff --git a/paypal_test.go b/paypal_test.go new file mode 100644 index 0000000..44769e6 --- /dev/null +++ b/paypal_test.go @@ -0,0 +1,25 @@ +package Ewp + +import ( + "testing" + "fmt" +) + +func TestEncryption(t *testing.T) { + + mdata := &CryptData{ + Business: "example@example.com", + ItemName: "demo", + ItemNumber: "demo123", + Amount: 123.20, + Invoice: "ONL-OrderID", + ReturnUrl: "https://www.google.com/", + + } + + m := NewPaypalEwp(EwpOptions{Certificate:"fixtures/demo.pem", PrivateKey:"fixtures/demo.key", PaypalCertificateFile: "fixtures/paypal.pem", CertificateID: "PAYPAL-CertID"}) + fmt.Printf(">>>%#v", m.GetError()) + fmt.Println("Result:") + fmt.Println(m.Generate(mdata)) + fmt.Printf(">>>%#v", m.GetError()) +} \ No newline at end of file