├── .gitignore ├── .idea ├── compiler.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── file_signer.go ├── file_signer_test.go ├── go.mod ├── go.sum ├── mem_signer.go ├── mem_signer_test.go ├── pass.go ├── pass_test.go ├── passkit.iml ├── semantics.go ├── signing.go ├── signing_test.go ├── templates.go ├── test ├── StoreCard.raw │ ├── .ignored_file │ ├── en.lproj │ │ ├── .ignored_file │ │ ├── logo.png │ │ └── logo@2x.png │ ├── icon.png │ ├── icon@2x.png │ ├── logo.png │ ├── logo@2x.png │ ├── strip.png │ └── strip@2x.png ├── ca.pem ├── log4j.properties ├── pass2.json ├── passbook │ ├── AppleWWDRCA.cer │ ├── ca.pem │ └── passkit.p12 ├── server-certs.pem └── server-key.pem └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Sensitive or high-churn files 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | .idea/**/dbnavigator.xml 20 | 21 | # Gradle 22 | .idea/**/gradle.xml 23 | .idea/**/libraries 24 | 25 | # CMake 26 | cmake-build-debug/ 27 | cmake-build-release/ 28 | 29 | # Mongo Explorer plugin 30 | .idea/**/mongoSettings.xml 31 | 32 | # File-based project format 33 | *.iws 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | 53 | # Editor-based Rest Client 54 | .idea/httpRequests 55 | ### Go template 56 | # Binaries for programs and plugins 57 | *.exe 58 | *.exe~ 59 | *.dll 60 | *.so 61 | *.dylib 62 | 63 | # Test binary, build with `go test -c` 64 | *.test 65 | 66 | # Output of the go coverage tool, specifically when used with LiteIDE 67 | *.out 68 | 69 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alvin Baena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PassKit 2 | 3 | This is a library for generating Apple Wallet PKPasses. 4 | 5 | ## How to use 6 | 7 | This library was heavily inspired by [drallgood's jpasskit library](https://github.com/drallgood/jpasskit) which was 8 | written in Java, so the objects and functions are very similar to the ones available on jpasskit. 9 | 10 | ### Define a pass 11 | 12 | To define a pass you use the `Pass` struct, which represents 13 | the [pass.json](https://developer.apple.com/documentation/walletpasses/pass) file. This struct is modeled as closely as 14 | possible to the json file, so adding data is straightforward: 15 | 16 | ```go 17 | c := passkit.NewBoardingPass(passkit.TransitTypeAir) 18 | 19 | // Utility functions for adding fields to a pass 20 | c.AddHeaderField(passkit.Field{ 21 | Key: "your_head_key", 22 | Label: "your_displayable_head_label", 23 | Value:"value", 24 | }) 25 | c.AddPrimaryFields(passkit.Field{ 26 | Key: "your_prim_key", 27 | Label: "your_displayable_prim_label", 28 | Value:"value", 29 | }) 30 | c.AddSecondaryFields(passkit.Field{ 31 | Key: "your_sec_key", 32 | Label: "your_displayable_sec_label", 33 | Value:"value", 34 | }) 35 | c.AddAuxiliaryFields(passkit.Field{ 36 | Key: "your_aux_key", 37 | Label: "your_displayable_aux_label", 38 | Value:"value", 39 | }) 40 | c.AddBackFields(passkit.Field{ 41 | Key: "your_back_key", 42 | Label: "your_displayable_back_label", 43 | Value:"value", 44 | }) 45 | 46 | boarding := time.Date(2023, 1, 1, 23, 50, 00, 00, time.UTC) 47 | 48 | pass := passkit.Pass{ 49 | FormatVersion: 1, 50 | TeamIdentifier: "TEAMID", 51 | PassTypeIdentifier: "pass.type.id", 52 | AuthenticationToken: "123141lkjdasj12314", 53 | OrganizationName: "Your Organization", 54 | SerialNumber: "1234", 55 | Description: "test", 56 | BoardingPass: c, 57 | Semantics: []passkit.SemanticTag{ 58 | { 59 | AirlineCode: "AA1234", 60 | TransitProvider: "American Airlines", 61 | DepartureAirportCode: "LAX", 62 | DepartureAirportName: "Los Angeles International Airport", 63 | DepartureGate: "28", 64 | DepartureTerminal: "2", 65 | DestinationAirportCode: "LGA", 66 | DestinationAirportName: "LaGuardia Airport", 67 | DestinationGate: "12", 68 | DestinationTerminal: "1", 69 | TransitStatus: "On Time", 70 | OriginalBoardingDate: &boarding, 71 | }, 72 | }, 73 | Barcodes: []passkit.Barcode{ 74 | { 75 | Format: passkit.BarcodeFormatPDF417, 76 | Message: "1312312312312312312312312312", 77 | MessageEncoding: "utf-8", 78 | }, 79 | }, 80 | } 81 | ``` 82 | 83 | ### Templates 84 | 85 | Passes contain additional data that has to be included in the final, signed pass, like images (icons, 86 | logos, background images) and translations. Usually, passes of the same type share images and translations. This shared 87 | information is what I call a `PassTemplate`, so that every generated pass of a type has the same images and whatnot. 88 | 89 | The template contents are defined as described by the 90 | [Apple Wallet developer documentation](https://developer.apple.com/documentation/walletpasses/creating_the_source_for_a_pass). 91 | 92 | To create the pass structure you need a `PassTemplate` instance, either with streams (using `InMemoryPassTemplate`) or 93 | with files (using `FolderPassTemplate`). 94 | 95 | #### Using files 96 | 97 | To create the pass bundle create an instance of `FolderPassTemplate` using the absolute file path of the folder 98 | that contain the pass images and translations: 99 | 100 | ```go 101 | template := passkit.NewFolderPassTemplate("/home/user/pass") 102 | ``` 103 | 104 | All the files in the folder will be added to the bundled pass exactly as provided, which means they _must_ align 105 | to the naming and location conventions described in the 106 | [Apple Wallet developer documentation](https://developer.apple.com/documentation/walletpasses/creating_the_source_for_a_pass). 107 | 108 | #### Using streams (In Memory) 109 | 110 | The second approach is more flexible, having the option of loading files from data streams, or downloaded from 111 | a public URL: 112 | 113 | ```go 114 | template := passkit.NewInMemoryPassTemplate() 115 | 116 | template.AddFileBytes(passkit.BundleThumbnail, bytes) 117 | template.AddFileBytesLocalized(passkit.BundleIcon, "en", bytes) 118 | err := template.AddFileFromURL(passkit.BundleLogo, "https://example.com/file.png") 119 | err := template.AddFileFromURLLocalized(passkit.BundleLogo, "en", "https://example.com/file.png") 120 | err := template.AddAllFiles("/home/user/pass") 121 | ``` 122 | 123 | **Note**: There are no checks that the contents of a provided file are valid. If a PDF file is provided, but is 124 | named `icon.png`, when loading the pass on a device, it will probably fail. The `InMemoryPassTemplate` doesn't 125 | provide any authentication for the downloads, so the URLs used must be public for the download to work as expected. The 126 | downloads use a default `http.Client` without any SSL configuration, so if the download is from an HTTPS site, the 127 | certificate must be able to be validated by the system's certificate store. If it cannot, the download will fail with an 128 | SSL error. 129 | 130 | ### Signing and zipping a pass 131 | 132 | As all passes [need to be signed when bundled](https://developer.apple.com/documentation/walletpasses/building_a_pass) 133 | we need to use a `Signer` instance. There are two types of signer: 134 | 135 | * `FileBasedSigner`: creates a temp folder to store the signed pass file contents. 136 | * `MemoryBasedSigner`: keeps the signed pass file contents in memory. 137 | 138 | To use any of the `Signer` instances you need to provide an instance of `SigningInformation`. `SigningInformation` 139 | defines the certificates used to generate the `signature` file bundled with every pass. There are two ways to obtain an 140 | instance. Either by reading the certificates from the filesystem, or from already loaded bytes in memory: 141 | 142 | ```go 143 | // Using the certificate files from your filesystem 144 | signInfo, err := passkit.LoadSigningInformationFromFiles("/home/user/pass_cert.p12", "pass_cert_password", "/home/user/AppleWWDRCA.cer") 145 | // Using the bytes from the certificates, loaded from a database or vault, for example. 146 | signInfo, err := passkit.LoadSigningInformationFromBytes(passCertBytes, "pass_cert_password", wwdrcaBytes) 147 | ``` 148 | 149 | **Important**: The provided certificates _must_ be encoded in DER form. If the files are encoded as PEM the signature 150 | generation will fail. Errors will also be returned if the certificates are invalid (expired, not x509 certs, etc.) 151 | 152 | ## Bundling the pass 153 | 154 | Finally, to create the signed pass bundle you use the `Pass`, `Signer`, `SigningInformation`, and `PassTemplate` 155 | instances created previously, like so: 156 | 157 | ```go 158 | signer := passkit.NewMemoryBasedSigner() 159 | signInfo, err := passkit.LoadSigningInformationFromFiles("/home/user/pass_cert.p12", "pass_cert_password", "/home/user/AppleWWDRCA.cer") 160 | if err != nil { 161 | panic(err) 162 | } 163 | 164 | z, err := signer.CreateSignedAndZippedPassArchive(&pass, template, signInfo) 165 | if err != nil { 166 | panic(err) 167 | } 168 | 169 | err = os.WriteFile("/home/user/pass.pkpass", z, 0644) 170 | if err != nil { 171 | panic(err) 172 | } 173 | ``` 174 | 175 | After this step the pass bundle is ready to be distributed as you see fit. 176 | 177 | ## Contributing 178 | 179 | Right now I'm not really working on a project where this library is being actively used, so any bugs are hard for me 180 | to detect and fix. That's why this project is open to contributions. Just make a Pull Request with fixes 181 | or any other feature request, and I will probably accept them and merge them (I will at least look at your code before 182 | approving anything). 183 | -------------------------------------------------------------------------------- /file_signer.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "crypto/sha1" 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | type fileSigner struct { 14 | } 15 | 16 | func NewFileBasedSigner() Signer { 17 | return &fileSigner{} 18 | } 19 | 20 | func (f *fileSigner) CreateSignedAndZippedPassArchive(p *Pass, t PassTemplate, i *SigningInformation) ([]byte, error) { 21 | return f.CreateSignedAndZippedPersonalizedPassArchive(p, nil, t, i) 22 | } 23 | 24 | func (f *fileSigner) CreateSignedAndZippedPersonalizedPassArchive(p *Pass, pz *Personalization, t PassTemplate, i *SigningInformation) ([]byte, error) { 25 | dir, err := os.MkdirTemp("", "pass") 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | if err := t.ProvisionPassAtDirectory(dir); err != nil { 31 | return nil, err 32 | } 33 | 34 | if err := f.createPassJSONFile(p, dir); err != nil { 35 | return nil, err 36 | } 37 | 38 | if pz != nil { 39 | if err := f.createPersonalizationJSONFile(pz, dir); err != nil { 40 | return nil, err 41 | } 42 | } 43 | 44 | mfst, err := f.createManifestJSONFile(dir) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | signedMfst, err := signManifestFile(mfst, i) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | err = os.WriteFile(signatureFileName, signedMfst, 0644) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | z, err := f.createZipFile(dir) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | //Fail silently 65 | _ = os.RemoveAll(dir) 66 | return z, nil 67 | } 68 | 69 | func (f *fileSigner) SignManifestFile(manifestJson []byte, i *SigningInformation) ([]byte, error) { 70 | return signManifestFile(manifestJson, i) 71 | } 72 | 73 | func (f *fileSigner) createPassJSONFile(p *Pass, tmpDir string) error { 74 | if !p.IsValid() { 75 | return fmt.Errorf("%v", p.GetValidationErrors()) 76 | } 77 | 78 | b, err := p.toJSON() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | return os.WriteFile(filepath.Join(tmpDir, passJsonFileName), b, 0644) 84 | } 85 | 86 | func (f *fileSigner) createPersonalizationJSONFile(pz *Personalization, tmpDir string) error { 87 | b, err := pz.toJSON() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return os.WriteFile(filepath.Join(tmpDir, personalizationJsonFileName), b, 0644) 93 | } 94 | 95 | func (f *fileSigner) createManifestJSONFile(tmpDir string) ([]byte, error) { 96 | m, err := f.hashFiles(tmpDir) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | bm, err := json.Marshal(m) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | err = os.WriteFile(manifestJsonFileName, bm, 0644) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return bm, nil 112 | } 113 | 114 | func (f *fileSigner) hashFiles(tmpDir string) (map[string]string, error) { 115 | files, err := loadDir(tmpDir) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | ret := make(map[string]string) 121 | for name, data := range files { 122 | hash := sha1.Sum(data) 123 | ret[filepath.Base(name)] = fmt.Sprintf("%x", hash) 124 | } 125 | 126 | return ret, nil 127 | } 128 | 129 | func (f *fileSigner) createZipFile(tmpDir string) ([]byte, error) { 130 | buf := new(bytes.Buffer) 131 | w := zip.NewWriter(buf) 132 | 133 | err := addFiles(w, tmpDir, "") 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | if err := w.Close(); err != nil { 139 | return nil, err 140 | } 141 | 142 | return buf.Bytes(), nil 143 | } 144 | -------------------------------------------------------------------------------- /file_signer_test.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | // 4 | //js, err := ioutil.ReadFile(filepath.Join("test", "pass2.json")) 5 | //if err != nil { 6 | //t.Errorf("could not load json file, %v", err) 7 | //} 8 | // 9 | //var pass Pass 10 | //err = json.Unmarshal(js, &pass) 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alvinbaena/passkit 2 | 3 | go 1.17 4 | 5 | require ( 6 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 7 | gopkg.in/go-playground/colors.v1 v1.2.0 8 | software.sslmate.com/src/go-pkcs12 v0.4.0 9 | ) 10 | 11 | require golang.org/x/crypto v0.38.0 // indirect 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 2 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 3 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= 4 | go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= 5 | go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= 6 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 7 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 8 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 9 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 10 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 11 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 12 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 13 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 14 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 15 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 16 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 17 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 18 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 21 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 22 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 23 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 24 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 25 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 26 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 27 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 31 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 32 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 33 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 34 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 35 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 46 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 47 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 48 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 49 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 50 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 51 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 52 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 53 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 54 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 55 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 56 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 57 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 58 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 59 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 60 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 61 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 62 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 63 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 64 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 65 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 66 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 67 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 68 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 69 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 70 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 71 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 72 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | gopkg.in/go-playground/colors.v1 v1.2.0 h1:SPweMUve+ywPrfwao+UvfD5Ah78aOLUkT5RlJiZn52c= 74 | gopkg.in/go-playground/colors.v1 v1.2.0/go.mod h1:AvbqcMpNXVl5gBrM20jBm3VjjKBbH/kI5UnqjU7lxFI= 75 | software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 76 | software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= 77 | software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 78 | -------------------------------------------------------------------------------- /mem_signer.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "crypto/sha1" 7 | "encoding/json" 8 | "fmt" 9 | ) 10 | 11 | type memorySigner struct { 12 | } 13 | 14 | func NewMemoryBasedSigner() Signer { 15 | return &memorySigner{} 16 | } 17 | 18 | func (m *memorySigner) CreateSignedAndZippedPassArchive(p *Pass, t PassTemplate, i *SigningInformation) ([]byte, error) { 19 | return m.CreateSignedAndZippedPersonalizedPassArchive(p, nil, t, i) 20 | } 21 | 22 | func (m *memorySigner) CreateSignedAndZippedPersonalizedPassArchive(p *Pass, pz *Personalization, t PassTemplate, i *SigningInformation) ([]byte, error) { 23 | originalFiles, err := t.GetAllFiles() 24 | if err != nil { 25 | return nil, err 26 | } 27 | files := m.makeFilesCopy(originalFiles) 28 | 29 | if !p.IsValid() { 30 | return nil, fmt.Errorf("%v", p.GetValidationErrors()) 31 | } 32 | 33 | pb, err := p.toJSON() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | files[passJsonFileName] = pb 39 | 40 | if pz != nil { 41 | if !pz.IsValid() { 42 | return nil, fmt.Errorf("%v", pz.GetValidationErrors()) 43 | } 44 | 45 | pzb, err := pz.toJSON() 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | files[personalizationJsonFileName] = pzb 51 | } 52 | 53 | msftHash, err := m.hashFiles(files) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | mfst, err := json.Marshal(msftHash) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | files[manifestJsonFileName] = mfst 64 | 65 | signedMfst, err := signManifestFile(mfst, i) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | files[signatureFileName] = signedMfst 71 | 72 | z, err := m.createZipFile(files) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return z, nil 78 | } 79 | 80 | func (m *memorySigner) SignManifestFile(manifestJson []byte, i *SigningInformation) ([]byte, error) { 81 | return signManifestFile(manifestJson, i) 82 | } 83 | 84 | func (m *memorySigner) hashFiles(files map[string][]byte) (map[string]string, error) { 85 | ret := make(map[string]string) 86 | for name, data := range files { 87 | hash := sha1.Sum(data) 88 | ret[name] = fmt.Sprintf("%x", hash) 89 | } 90 | 91 | return ret, nil 92 | } 93 | 94 | func (m *memorySigner) createZipFile(files map[string][]byte) ([]byte, error) { 95 | buf := new(bytes.Buffer) 96 | w := zip.NewWriter(buf) 97 | 98 | for name, data := range files { 99 | f, err := w.Create(name) 100 | if err != nil { 101 | return nil, err 102 | } 103 | _, err = f.Write(data) 104 | if err != nil { 105 | return nil, err 106 | } 107 | } 108 | 109 | if err := w.Close(); err != nil { 110 | return nil, err 111 | } 112 | 113 | return buf.Bytes(), nil 114 | } 115 | 116 | func (m *memorySigner) makeFilesCopy(files map[string][]byte) map[string][]byte { 117 | filesCopy := make(map[string][]byte, len(files)) 118 | for k := range files { 119 | filesCopy[k] = files[k] 120 | } 121 | 122 | return filesCopy 123 | } 124 | -------------------------------------------------------------------------------- /mem_signer_test.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | -------------------------------------------------------------------------------- /pass.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gopkg.in/go-playground/colors.v1" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type BarcodeFormat string 13 | type TextAlignment string 14 | type DataDetectorType string 15 | type DateStyle string 16 | type NumberStyle string 17 | type PassPersonalizationField string 18 | type TransitType string 19 | type EventType string 20 | 21 | const ( 22 | expectedAuthTokenLen = 16 23 | 24 | TextAlignmentLeft TextAlignment = "PKTextAlignmentLeft" 25 | TextAlignmentCenter TextAlignment = "PKTextAlignmentCenter" 26 | TextAlignmentRight TextAlignment = "PKTextAlignmentRight" 27 | TextAlignmentNatural TextAlignment = "PKTextAlignmentNatural" 28 | 29 | BarcodeFormatQR BarcodeFormat = "PKBarcodeFormatQR" 30 | BarcodeFormatPDF417 BarcodeFormat = "PKBarcodeFormatPDF417" 31 | BarcodeFormatAztec BarcodeFormat = "PKBarcodeFormatAztec" 32 | BarcodeFormatCode128 BarcodeFormat = "PKBarcodeFormatCode128" 33 | 34 | DataDetectorTypePhoneNumber DataDetectorType = "PKDataDetectorTypePhoneNumber" 35 | DataDetectorTypeLink DataDetectorType = "PKDataDetectorTypeLink" 36 | DataDetectorTypeAddress DataDetectorType = "PKDataDetectorTypeAddress" 37 | DataDetectorTypeCalendarEvent DataDetectorType = "PKDataDetectorTypeCalendarEvent" 38 | 39 | DateStyleNone DateStyle = "PKDateStyleNone" 40 | DateStyleShort DateStyle = "PKDateStyleShort" 41 | DateStyleMedium DateStyle = "PKDateStyleMedium" 42 | DateStyleLong DateStyle = "PKDateStyleLong" 43 | DateStyleFull DateStyle = "PKDateStyleFull" 44 | 45 | NumberStyleDecimal NumberStyle = "PKNumberStyleDecimal" 46 | NumberStylePercent NumberStyle = "PKNumberStylePercent" 47 | NumberStyleScientific NumberStyle = "PKNumberStyleScientific" 48 | NumberStyleSpellOut NumberStyle = "PKNumberStyleSpellOut" 49 | 50 | PassPersonalizationFieldName PassPersonalizationField = "PKPassPersonalizationFieldName" 51 | PassPersonalizationFieldPostalCode PassPersonalizationField = "PKPassPersonalizationFieldPostalCode" 52 | PassPersonalizationFieldEmailAddress PassPersonalizationField = "PKPassPersonalizationFieldEmailAddress" 53 | PassPersonalizationFieldPhoneNumber PassPersonalizationField = "PKPassPersonalizationFieldPhoneNumber" 54 | 55 | TransitTypeAir TransitType = "PKTransitTypeAir" 56 | TransitTypeBoat TransitType = "PKTransitTypeBoat" 57 | TransitTypeBus TransitType = "PKTransitTypeBus" 58 | TransitTypeGeneric TransitType = "PKTransitTypeGeneric" 59 | TransitTypeTrain TransitType = "PKTransitTypeTrain" 60 | 61 | EventTypeGeneric EventType = "PKEventTypeGeneric" 62 | EventTypeLivePerformance EventType = "PKEventTypeLivePerformance" 63 | EventTypeMovie EventType = "PKEventTypeMovie" 64 | EventTypeSports EventType = "PKEventTypeSports" 65 | EventTypeConference EventType = "PKEventTypeConference" 66 | EventTypeConvention EventType = "PKEventTypeConvention" 67 | EventTypeWorkshop EventType = "PKEventTypeWorkshop" 68 | EventTypeSocialGathering EventType = "PKEventTypeSocialGathering" 69 | ) 70 | 71 | var ( 72 | BarcodeTypesBeforeIos9 = [3]BarcodeFormat{BarcodeFormatQR, BarcodeFormatPDF417, BarcodeFormatAztec} 73 | ) 74 | 75 | type Validateable interface { 76 | IsValid() bool 77 | GetValidationErrors() []string 78 | } 79 | 80 | // Pass Representation of https://developer.apple.com/documentation/walletpasses/pass 81 | type Pass struct { 82 | FormatVersion int `json:"formatVersion,omitempty"` 83 | SerialNumber string `json:"serialNumber,omitempty"` 84 | PassTypeIdentifier string `json:"passTypeIdentifier,omitempty"` 85 | WebServiceURL string `json:"webServiceURL,omitempty"` 86 | AuthenticationToken string `json:"authenticationToken,omitempty"` 87 | Description string `json:"description,omitempty"` 88 | TeamIdentifier string `json:"teamIdentifier,omitempty"` 89 | OrganizationName string `json:"organizationName,omitempty"` 90 | LogoText string `json:"logoText,omitempty"` 91 | ForegroundColor string `json:"foregroundColor,omitempty"` 92 | BackgroundColor string `json:"backgroundColor,omitempty"` 93 | LabelColor string `json:"labelColor,omitempty"` 94 | GroupingIdentifier string `json:"groupingIdentifier,omitempty"` 95 | Beacons []Beacon `json:"beacons,omitempty"` 96 | Locations []Location `json:"locations,omitempty"` 97 | Barcodes []Barcode `json:"barcodes,omitempty"` 98 | EventTicket *EventTicket `json:"eventTicket,omitempty"` 99 | Coupon *Coupon `json:"coupon,omitempty"` 100 | StoreCard *StoreCard `json:"storeCard,omitempty"` 101 | BoardingPass *BoardingPass `json:"boardingPass,omitempty"` 102 | Generic *GenericPass `json:"generic,omitempty"` 103 | AppLaunchURL string `json:"appLaunchURL,omitempty"` 104 | AssociatedStoreIdentifiers []int64 `json:"associatedStoreIdentifiers,omitempty"` 105 | UserInfo map[string]interface{} `json:"userInfo,omitempty"` 106 | MaxDistance int64 `json:"maxDistance,omitempty"` 107 | RelevantDate *time.Time `json:"relevantDate,omitempty"` 108 | ExpirationDate *time.Time `json:"expirationDate,omitempty"` 109 | Voided bool `json:"voided,omitempty"` 110 | Nfc *NFC `json:"nfc,omitempty"` 111 | SharingProhibited bool `json:"sharingProhibited,omitempty"` 112 | Semantics *SemanticTag `json:"semantics,omitempty"` 113 | 114 | //Private 115 | associatedApps []PWAssociatedApp 116 | } 117 | 118 | func (p *Pass) SetForegroundColorHex(hex string) error { 119 | h, err := colors.ParseHEX(hex) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | p.ForegroundColor = h.ToRGB().String() 125 | return nil 126 | } 127 | 128 | func (p *Pass) SetForegroundColorRGB(r, g, b uint8) error { 129 | rgb, _ := colors.RGB(r, g, b) 130 | 131 | p.ForegroundColor = rgb.String() 132 | return nil 133 | } 134 | 135 | func (p *Pass) SetBackgroundColorHex(hex string) error { 136 | h, err := colors.ParseHEX(hex) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | p.BackgroundColor = h.ToRGB().String() 142 | return nil 143 | } 144 | 145 | func (p *Pass) SetBackgroundColorRGB(r, g, b uint8) error { 146 | rgb, _ := colors.RGB(r, g, b) 147 | 148 | p.BackgroundColor = rgb.String() 149 | return nil 150 | } 151 | 152 | func (p *Pass) SetLabelColorHex(hex string) error { 153 | h, err := colors.ParseHEX(hex) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | p.LabelColor = h.ToRGB().String() 159 | return nil 160 | } 161 | 162 | func (p *Pass) SetLabelColorRGB(r, g, b uint8) error { 163 | rgb, _ := colors.RGB(r, g, b) 164 | 165 | p.LabelColor = rgb.String() 166 | return nil 167 | } 168 | 169 | func (p *Pass) toJSON() ([]byte, error) { 170 | return json.Marshal(p) 171 | } 172 | 173 | func (p *Pass) IsValid() bool { 174 | return len(p.GetValidationErrors()) == 0 175 | } 176 | 177 | func (p *Pass) GetValidationErrors() []string { 178 | var validationErrors []string 179 | 180 | if strings.TrimSpace(p.SerialNumber) == "" || strings.TrimSpace(p.PassTypeIdentifier) == "" || 181 | strings.TrimSpace(p.TeamIdentifier) == "" || strings.TrimSpace(p.Description) == "" || 182 | p.FormatVersion == 0 || strings.TrimSpace(p.OrganizationName) == "" { 183 | 184 | validationErrors = append(validationErrors, fmt.Sprintf("Pass: Not all required Fields are set. SerialNumber: %q, PassTypeIdentifier: %q, teamIdentifier: %q, Description: ,%q, FormatVersion: %q, OrganizationName: %q", p.SerialNumber, p.PassTypeIdentifier, p.TeamIdentifier, p.Description, p.FormatVersion, p.OrganizationName)) 185 | } 186 | 187 | if p.EventTicket == nil && p.BoardingPass == nil && p.Coupon == nil && p.StoreCard == nil && p.Generic == nil { 188 | validationErrors = append(validationErrors, fmt.Sprintf("Pass: No pass was set. EventTicket: %v, BoardingPass: %v, Coupon: %v, StoreCard: %v, Generic: %v", p.EventTicket, p.BoardingPass, p.Coupon, p.StoreCard, p.Generic)) 189 | } 190 | 191 | if p.EventTicket != nil && (p.BoardingPass != nil || p.Coupon != nil || p.StoreCard != nil || p.Generic != nil) { 192 | validationErrors = append(validationErrors, "Pass: Only one pass should be set") 193 | 194 | } else if p.BoardingPass != nil && (p.EventTicket != nil || p.Coupon != nil || p.StoreCard != nil || p.Generic != nil) { 195 | validationErrors = append(validationErrors, "Pass: Only one pass should be set") 196 | 197 | } else if p.Coupon != nil && (p.BoardingPass != nil || p.EventTicket != nil || p.StoreCard != nil || p.Generic != nil) { 198 | validationErrors = append(validationErrors, "Pass: Only one pass should be set") 199 | 200 | } else if p.StoreCard != nil && (p.BoardingPass != nil || p.Coupon != nil || p.EventTicket != nil || p.Generic != nil) { 201 | validationErrors = append(validationErrors, "Pass: Only one pass should be set") 202 | 203 | } else if p.Generic != nil && (p.BoardingPass != nil || p.Coupon != nil || p.StoreCard != nil || p.EventTicket != nil) { 204 | validationErrors = append(validationErrors, "Pass: Only one pass should be set") 205 | } 206 | 207 | if p.WebServiceURL != "" && (len(p.AuthenticationToken) < expectedAuthTokenLen) { 208 | validationErrors = append(validationErrors, 209 | "Pass: The authenticationToken needs to be at least "+strconv.Itoa(expectedAuthTokenLen)+" characters long") 210 | } 211 | 212 | if p.EventTicket != nil && !p.EventTicket.IsValid() { 213 | validationErrors = append(validationErrors, p.EventTicket.GetValidationErrors()...) 214 | } else if p.BoardingPass != nil && !p.BoardingPass.IsValid() { 215 | validationErrors = append(validationErrors, p.BoardingPass.GetValidationErrors()...) 216 | } else if p.Coupon != nil && !p.Coupon.IsValid() { 217 | validationErrors = append(validationErrors, p.Coupon.GetValidationErrors()...) 218 | } else if p.StoreCard != nil && !p.StoreCard.IsValid() { 219 | validationErrors = append(validationErrors, p.StoreCard.GetValidationErrors()...) 220 | } else if p.Generic != nil && !p.Generic.IsValid() { 221 | validationErrors = append(validationErrors, p.Generic.GetValidationErrors()...) 222 | } 223 | 224 | // If appLaunchURL key is present, the associatedStoreIdentifiers key must also be present 225 | if p.AppLaunchURL != "" && len(p.AssociatedStoreIdentifiers) == 0 { 226 | validationErrors = append(validationErrors, "Pass: The appLaunchURL requires associatedStoreIdentifiers to be specified") 227 | } 228 | 229 | if !(p.EventTicket == nil && p.BoardingPass == nil && p.Coupon == nil && p.StoreCard == nil && p.Generic == nil) { 230 | // groupingIdentifier key is optional for event tickets and boarding passes; otherwise not allowed 231 | if strings.TrimSpace(p.GroupingIdentifier) != "" && p.EventTicket == nil && p.BoardingPass == nil { 232 | validationErrors = append(validationErrors, "Pass: The groupingIdentifier is optional for event tickets and boarding passes, otherwise not allowed") 233 | } 234 | } 235 | 236 | if p.Beacons != nil { 237 | for _, b := range p.Beacons { 238 | if !b.IsValid() { 239 | validationErrors = append(validationErrors, b.GetValidationErrors()...) 240 | } 241 | } 242 | } 243 | 244 | if p.Barcodes != nil { 245 | for _, b := range p.Barcodes { 246 | if !b.IsValid() { 247 | validationErrors = append(validationErrors, b.GetValidationErrors()...) 248 | } 249 | } 250 | } 251 | 252 | if p.Semantics != nil && !p.Semantics.IsValid() { 253 | validationErrors = append(validationErrors, p.Semantics.GetValidationErrors()...) 254 | } 255 | 256 | return validationErrors 257 | } 258 | 259 | func NewGenericPass() *GenericPass { 260 | return &GenericPass{} 261 | } 262 | 263 | // GenericPass Representation of https://developer.apple.com/documentation/walletpasses/pass/generic 264 | type GenericPass struct { 265 | HeaderFields []Field `json:"headerFields,omitempty"` 266 | PrimaryFields []Field `json:"primaryFields,omitempty"` 267 | SecondaryFields []Field `json:"secondaryFields,omitempty"` 268 | AuxiliaryFields []Field `json:"auxiliaryFields,omitempty"` 269 | BackFields []Field `json:"backFields,omitempty"` 270 | } 271 | 272 | func (gp *GenericPass) AddHeaderField(field Field) { 273 | gp.HeaderFields = append(gp.HeaderFields, field) 274 | } 275 | 276 | func (gp *GenericPass) AddPrimaryFields(field Field) { 277 | gp.PrimaryFields = append(gp.PrimaryFields, field) 278 | } 279 | 280 | func (gp *GenericPass) AddSecondaryFields(field Field) { 281 | gp.SecondaryFields = append(gp.SecondaryFields, field) 282 | } 283 | 284 | func (gp *GenericPass) AddAuxiliaryFields(field Field) { 285 | gp.AuxiliaryFields = append(gp.AuxiliaryFields, field) 286 | } 287 | 288 | func (gp *GenericPass) AddBackFields(field Field) { 289 | gp.BackFields = append(gp.BackFields, field) 290 | } 291 | 292 | func (gp *GenericPass) IsValid() bool { 293 | return len(gp.GetValidationErrors()) == 0 294 | } 295 | 296 | func (gp *GenericPass) GetValidationErrors() []string { 297 | var validationErrors []string 298 | 299 | var fields [][]Field 300 | fields = append(fields, gp.HeaderFields) 301 | fields = append(fields, gp.PrimaryFields) 302 | fields = append(fields, gp.SecondaryFields) 303 | fields = append(fields, gp.AuxiliaryFields) 304 | fields = append(fields, gp.BackFields) 305 | 306 | for _, fieldList := range fields { 307 | for _, field := range fieldList { 308 | if !field.IsValid() { 309 | validationErrors = append(validationErrors, field.GetValidationErrors()...) 310 | } 311 | } 312 | } 313 | 314 | return validationErrors 315 | } 316 | 317 | // BoardingPass Representation of https://developer.apple.com/documentation/walletpasses/pass/boardingpass 318 | type BoardingPass struct { 319 | *GenericPass 320 | TransitType TransitType `json:"transitType,omitempty"` 321 | } 322 | 323 | func NewBoardingPass(transitType TransitType) *BoardingPass { 324 | return &BoardingPass{GenericPass: NewGenericPass(), TransitType: transitType} 325 | } 326 | 327 | func (b *BoardingPass) IsValid() bool { 328 | return len(b.GetValidationErrors()) == 0 329 | } 330 | 331 | func (b *BoardingPass) GetValidationErrors() []string { 332 | var validationErrors []string 333 | 334 | validationErrors = append(validationErrors, b.GenericPass.GetValidationErrors()...) 335 | if string(b.TransitType) == "" { 336 | validationErrors = append(validationErrors, "BoardingPass: TransitType is not set") 337 | } 338 | 339 | return validationErrors 340 | } 341 | 342 | // Coupon Representation of https://developer.apple.com/documentation/walletpasses/pass/coupon 343 | type Coupon struct { 344 | *GenericPass 345 | } 346 | 347 | func NewCoupon() *Coupon { 348 | return &Coupon{GenericPass: NewGenericPass()} 349 | } 350 | 351 | // EventTicket Representation of https://developer.apple.com/documentation/walletpasses/pass/eventticket 352 | type EventTicket struct { 353 | *GenericPass 354 | } 355 | 356 | func NewEventTicket() *EventTicket { 357 | return &EventTicket{GenericPass: NewGenericPass()} 358 | } 359 | 360 | // StoreCard Representation of https://developer.apple.com/documentation/walletpasses/pass/storecard 361 | type StoreCard struct { 362 | *GenericPass 363 | } 364 | 365 | func NewStoreCard() *StoreCard { 366 | return &StoreCard{GenericPass: NewGenericPass()} 367 | } 368 | 369 | // Field Representation of https://developer.apple.com/documentation/walletpasses/passfieldcontent 370 | type Field struct { 371 | Key string `json:"key,omitempty"` 372 | Label string `json:"label,omitempty"` 373 | Value interface{} `json:"value,omitempty"` 374 | AttributedValue interface{} `json:"attributedValue,omitempty"` 375 | ChangeMessage string `json:"changeMessage,omitempty"` 376 | TextAlignment TextAlignment `json:"textAlignment,omitempty"` 377 | DataDetectorTypes []DataDetectorType `json:"dataDetectorTypes,omitempty"` 378 | CurrencyCode string `json:"currencyCode,omitempty"` 379 | NumberStyle NumberStyle `json:"numberStyle,omitempty"` 380 | DateStyle DateStyle `json:"dateStyle,omitempty"` 381 | TimeStyle DateStyle `json:"timeStyle,omitempty"` 382 | IsRelative bool `json:"isRelative,omitempty"` 383 | IgnoreTimeZone bool `json:"ignoresTimeZone,omitempty"` 384 | Semantics *SemanticTag `json:"semantics,omitempty"` 385 | Row int `json:"row,omitempty"` 386 | } 387 | 388 | func (f *Field) IsValid() bool { 389 | return len(f.GetValidationErrors()) == 0 390 | } 391 | 392 | func (f *Field) GetValidationErrors() []string { 393 | var validationErrors []string 394 | 395 | if f.Value == nil || f.Key == "" { 396 | validationErrors = append(validationErrors, fmt.Sprintf("Field: Not all required Fields are set. Key: %v Value: %v", f.Key, f.Value)) 397 | } 398 | 399 | if f.Value != nil { 400 | switch f.Value.(type) { 401 | case string: 402 | case int: 403 | case int8: 404 | case int16: 405 | case int32: 406 | case int64: 407 | case float32: 408 | case float64: 409 | case time.Time: 410 | default: 411 | validationErrors = append(validationErrors, "Field: Invalid value type. Allowed: string, int, float, time.Time") 412 | } 413 | } 414 | 415 | if strings.TrimSpace(f.CurrencyCode) != "" && string(f.NumberStyle) != "" { 416 | validationErrors = append(validationErrors, "Field: CurrencyCode and numberStyle are both set") 417 | } 418 | 419 | if (strings.TrimSpace(f.CurrencyCode) != "" || string(f.NumberStyle) != "") && (string(f.DateStyle) != "" || string(f.TimeStyle) != "") { 420 | validationErrors = append(validationErrors, "Field: Can't be number/currency and date at the same time") 421 | } 422 | 423 | if strings.TrimSpace(f.ChangeMessage) != "" && !strings.Contains(f.ChangeMessage, "%@") { 424 | validationErrors = append(validationErrors, "Field: ChangeMessage needs to contain %@ placeholder") 425 | } 426 | 427 | if strings.TrimSpace(f.CurrencyCode) != "" { 428 | switch f.Value.(type) { 429 | case int: 430 | case int8: 431 | case int16: 432 | case int32: 433 | case int64: 434 | case float32: 435 | case float64: 436 | default: 437 | validationErrors = append(validationErrors, "Field: When using currencies, the values have to be numbers") 438 | } 439 | } 440 | 441 | if f.Semantics != nil && !f.Semantics.IsValid() { 442 | validationErrors = append(validationErrors, f.Semantics.GetValidationErrors()...) 443 | } 444 | 445 | if f.Row != 0 && f.Row != 1 { 446 | validationErrors = append(validationErrors, "Row must be 0 or 1") 447 | } 448 | 449 | return validationErrors 450 | } 451 | 452 | // Beacon Representation of https://developer.apple.com/documentation/walletpasses/pass/beacons 453 | type Beacon struct { 454 | Major int `json:"major,omitempty"` 455 | Minor int `json:"minor,omitempty"` 456 | ProximityUUID string `json:"proximityUUID,omitempty"` 457 | RelevantText string `json:"relevantText,omitempty"` 458 | } 459 | 460 | func (b *Beacon) IsValid() bool { 461 | return len(b.GetValidationErrors()) == 0 462 | } 463 | 464 | func (b *Beacon) GetValidationErrors() []string { 465 | var validationErrors []string 466 | 467 | if strings.TrimSpace(b.ProximityUUID) == "" { 468 | validationErrors = append(validationErrors, "Beacon: Not all required Fields are set: proximityUUID") 469 | } 470 | 471 | return validationErrors 472 | } 473 | 474 | // Location Representation of https://developer.apple.com/documentation/walletpasses/pass/locations 475 | type Location struct { 476 | Latitude float64 `json:"latitude,omitempty"` 477 | Longitude float64 `json:"longitude,omitempty"` 478 | Altitude float64 `json:"altitude,omitempty"` 479 | RelevantText string `json:"relevantText,omitempty"` 480 | } 481 | 482 | func (l *Location) IsValid() bool { 483 | return len(l.GetValidationErrors()) == 0 484 | } 485 | 486 | func (l *Location) GetValidationErrors() []string { 487 | return []string{} 488 | } 489 | 490 | // Barcode Representation of https://developer.apple.com/documentation/walletpasses/pass/barcodes 491 | type Barcode struct { 492 | Format BarcodeFormat `json:"format,omitempty"` 493 | AltText string `json:"altText,omitempty"` 494 | Message string `json:"message,omitempty"` 495 | MessageEncoding string `json:"messageEncoding,omitempty"` 496 | } 497 | 498 | func (b *Barcode) IsValid() bool { 499 | return len(b.GetValidationErrors()) == 0 500 | } 501 | 502 | func (b *Barcode) GetValidationErrors() []string { 503 | var validationErrors []string 504 | 505 | if string(b.Format) == "" || strings.TrimSpace(b.Message) == "" || strings.TrimSpace(b.MessageEncoding) == "" { 506 | validationErrors = append(validationErrors, fmt.Sprintf("Barcode: Not all required Fields are set. Format: %v, Message: %v, MessageEncoding: %v, AltText: %v", b.Format, b.Message, b.MessageEncoding, b.AltText)) 507 | } 508 | 509 | return validationErrors 510 | } 511 | 512 | type PWAssociatedApp struct { 513 | Title string 514 | IdGooglePlay string 515 | IdAmazon string 516 | } 517 | 518 | func (a *PWAssociatedApp) IsValid() bool { 519 | return len(a.GetValidationErrors()) == 0 520 | } 521 | 522 | func (a *PWAssociatedApp) GetValidationErrors() []string { 523 | return []string{} 524 | } 525 | 526 | // NFC Representation of https://developer.apple.com/documentation/walletpasses/pass/nfc 527 | type NFC struct { 528 | Message string `json:"message,omitempty"` 529 | EncryptionPublicKey string `json:"encryptionPublicKey,omitempty"` 530 | RequiresAuthentication bool `json:"requiresAuthentication,omitempty"` 531 | } 532 | 533 | // Personalization Representation of https://developer.apple.com/documentation/walletpasses/personalize 534 | type Personalization struct { 535 | RequiredPersonalizationFields []PassPersonalizationField `json:"requiredPersonalizationFields"` 536 | Description string `json:"description"` 537 | TermsAndConditions string `json:"termsAndConditions"` 538 | } 539 | 540 | func (pz *Personalization) toJSON() ([]byte, error) { 541 | return json.Marshal(pz) 542 | } 543 | 544 | func (pz *Personalization) IsValid() bool { 545 | return len(pz.GetValidationErrors()) == 0 546 | } 547 | 548 | func (pz *Personalization) GetValidationErrors() []string { 549 | var validationErrors []string 550 | 551 | if len(pz.RequiredPersonalizationFields) == 0 { 552 | validationErrors = append(validationErrors, "Personalization: You need to provide at least one requiredPersonalizationField") 553 | } 554 | 555 | if strings.TrimSpace(pz.Description) == "" { 556 | validationErrors = append(validationErrors, "Personalization: You need to provide a description") 557 | } 558 | 559 | return validationErrors 560 | } 561 | -------------------------------------------------------------------------------- /pass_test.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func getBasicPersonalization() Personalization { 9 | return Personalization{ 10 | Description: "Description for pass", 11 | TermsAndConditions: "WTF", 12 | RequiredPersonalizationFields: []PassPersonalizationField{ 13 | PassPersonalizationFieldName, 14 | PassPersonalizationFieldEmailAddress, 15 | }, 16 | } 17 | } 18 | 19 | func TestPersonalization_GetSet(t *testing.T) { 20 | prs := getBasicPersonalization() 21 | 22 | if !prs.IsValid() { 23 | t.Errorf("Personalization should be valid. Reason: %v", prs.GetValidationErrors()) 24 | } 25 | 26 | if len(prs.RequiredPersonalizationFields) == 0 { 27 | t.Errorf("Personalization should have fields") 28 | } 29 | 30 | if len(prs.GetValidationErrors()) != 0 { 31 | t.Errorf("Personalization should not have errors. Have %v", len(prs.GetValidationErrors())) 32 | } 33 | } 34 | 35 | func TestPersonalization_OptionalFields(t *testing.T) { 36 | prs := getBasicPersonalization() 37 | prs.TermsAndConditions = "" 38 | 39 | if !prs.IsValid() { 40 | t.Errorf("Personalization should be valid. Reason: %v", prs.GetValidationErrors()) 41 | } 42 | 43 | if len(prs.RequiredPersonalizationFields) == 0 { 44 | t.Errorf("Personalization should have fields") 45 | } 46 | 47 | if len(prs.GetValidationErrors()) != 0 { 48 | t.Errorf("Personalization should not have errors. Have %v", len(prs.GetValidationErrors())) 49 | } 50 | } 51 | 52 | func TestPersonalization_Invalid(t *testing.T) { 53 | prs := getBasicPersonalization() 54 | prs.Description = "" 55 | 56 | t.Logf("%d, %v", len(prs.GetValidationErrors()), prs.GetValidationErrors()) 57 | if prs.IsValid() { 58 | t.Errorf("Personalization should be invalid") 59 | } 60 | 61 | if len(prs.GetValidationErrors()) != 1 { 62 | t.Errorf("Personalization should have only one validation error. Have %v", len(prs.GetValidationErrors())) 63 | } 64 | 65 | prs.RequiredPersonalizationFields = []PassPersonalizationField{} 66 | 67 | t.Logf("%d, %v", len(prs.GetValidationErrors()), prs.GetValidationErrors()) 68 | if prs.IsValid() { 69 | t.Errorf("Personalization should be invalid") 70 | } 71 | 72 | if len(prs.GetValidationErrors()) != 2 { 73 | t.Errorf("Personalization should have only two validation errors. Have %v", len(prs.GetValidationErrors())) 74 | } 75 | } 76 | 77 | func TestPersonalization_JSON(t *testing.T) { 78 | prs := getBasicPersonalization() 79 | _, err := prs.toJSON() 80 | 81 | if err != nil { 82 | t.Errorf("Error marshalling json. %v", err) 83 | } 84 | } 85 | 86 | func getBasicBarcode() Barcode { 87 | return Barcode{ 88 | Format: BarcodeFormatQR, 89 | AltText: "Alt Text", 90 | Message: "message", 91 | MessageEncoding: "utf-8", 92 | } 93 | } 94 | 95 | func TestBarcode_GetSet(t *testing.T) { 96 | bar := getBasicBarcode() 97 | 98 | if !bar.IsValid() { 99 | t.Errorf("Barcode should be valid. Reason: %v", bar.GetValidationErrors()) 100 | } 101 | 102 | if len(bar.GetValidationErrors()) != 0 { 103 | t.Errorf("Barcode should not have errors. Have %v", bar.GetValidationErrors()) 104 | } 105 | } 106 | 107 | func TestBarcode_NoMessage(t *testing.T) { 108 | bar := getBasicBarcode() 109 | bar.Message = "" 110 | 111 | t.Logf("%d, %v", len(bar.GetValidationErrors()), bar.GetValidationErrors()) 112 | if bar.IsValid() { 113 | t.Errorf("Barcode should be invalid") 114 | } 115 | 116 | if len(bar.GetValidationErrors()) != 1 { 117 | t.Errorf("Barcode should have only one error") 118 | } 119 | } 120 | 121 | func TestBarcode_NoAltText(t *testing.T) { 122 | bar := getBasicBarcode() 123 | bar.AltText = "" 124 | 125 | t.Logf("%d, %v", len(bar.GetValidationErrors()), bar.GetValidationErrors()) 126 | if bar.IsValid() { 127 | t.Errorf("Barcode should be invalid") 128 | } 129 | 130 | if len(bar.GetValidationErrors()) != 1 { 131 | t.Errorf("Barcode should have only one error") 132 | } 133 | } 134 | 135 | func TestBarcode_NoEncoding(t *testing.T) { 136 | bar := getBasicBarcode() 137 | bar.MessageEncoding = "" 138 | 139 | t.Logf("%d, %v", len(bar.GetValidationErrors()), bar.GetValidationErrors()) 140 | if bar.IsValid() { 141 | t.Errorf("Barcode should be invalid") 142 | } 143 | 144 | if len(bar.GetValidationErrors()) != 1 { 145 | t.Errorf("Barcode should have only one error") 146 | } 147 | } 148 | 149 | func TestBarcode_NoFormat(t *testing.T) { 150 | bar := getBasicBarcode() 151 | bar.Format = "" 152 | 153 | t.Logf("%d, %v", len(bar.GetValidationErrors()), bar.GetValidationErrors()) 154 | if bar.IsValid() { 155 | t.Errorf("Barcode should be invalid") 156 | } 157 | 158 | if len(bar.GetValidationErrors()) != 1 { 159 | t.Errorf("Barcode should have only one error") 160 | } 161 | } 162 | 163 | func getBasicBeacon() Beacon { 164 | return Beacon{ 165 | Major: 3, 166 | Minor: 29, 167 | ProximityUUID: "123456789-abcdefghijklmnopqrstuwxyz", 168 | RelevantText: "County of Zadar", 169 | } 170 | } 171 | 172 | func TestBeacon_GetSet(t *testing.T) { 173 | bea := getBasicBeacon() 174 | 175 | if !bea.IsValid() { 176 | t.Errorf("Beacon should be valid. Reason: %v", bea.GetValidationErrors()) 177 | } 178 | 179 | if len(bea.GetValidationErrors()) != 0 { 180 | t.Errorf("Beacon should not have errors. Have %v", len(bea.GetValidationErrors())) 181 | } 182 | } 183 | 184 | func TestBeacon_NoUUID(t *testing.T) { 185 | bea := getBasicBeacon() 186 | bea.ProximityUUID = "" 187 | 188 | t.Logf("%d, %v", len(bea.GetValidationErrors()), bea.GetValidationErrors()) 189 | if bea.IsValid() { 190 | t.Errorf("Beacon should be invalid") 191 | } 192 | 193 | if len(bea.GetValidationErrors()) != 1 { 194 | t.Errorf("Beacon should have one error. Have: %v", len(bea.GetValidationErrors())) 195 | } 196 | } 197 | 198 | func getBasicField() Field { 199 | return Field{ 200 | Key: "key", 201 | ChangeMessage: "Changed %@", 202 | Label: "Label", 203 | TextAlignment: TextAlignmentCenter, 204 | AttributedValue: "Edit my profile", 205 | DataDetectorTypes: []DataDetectorType{DataDetectorTypeAddress}, 206 | } 207 | } 208 | 209 | func TestField_GetSet(t *testing.T) { 210 | field := getBasicField() 211 | field.Value = "test" 212 | 213 | if !field.IsValid() { 214 | t.Errorf("Field should be valid. Reason: %v", field.GetValidationErrors()) 215 | } 216 | 217 | if len(field.GetValidationErrors()) != 0 { 218 | t.Errorf("Beacon should have no errors. Have: %v", len(field.GetValidationErrors())) 219 | } 220 | } 221 | 222 | func TestField_Values(t *testing.T) { 223 | field := getBasicField() 224 | field.Value = "Value" 225 | 226 | if !field.IsValid() { 227 | t.Errorf("Field should be valid. Reason: %v", field.GetValidationErrors()) 228 | } 229 | 230 | if len(field.GetValidationErrors()) != 0 { 231 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 232 | } 233 | 234 | field.Value = 1 235 | 236 | if !field.IsValid() { 237 | t.Errorf("Field should be valid") 238 | } 239 | 240 | if len(field.GetValidationErrors()) != 0 { 241 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 242 | } 243 | 244 | field.Value = 1.0 245 | 246 | if !field.IsValid() { 247 | t.Errorf("Field should be valid") 248 | } 249 | 250 | if len(field.GetValidationErrors()) != 0 { 251 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 252 | } 253 | 254 | field.Value = int8(1) 255 | 256 | if !field.IsValid() { 257 | t.Errorf("Field should be valid") 258 | } 259 | 260 | if len(field.GetValidationErrors()) != 0 { 261 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 262 | } 263 | 264 | field.Value = int8(1) 265 | 266 | if !field.IsValid() { 267 | t.Errorf("Field should be valid") 268 | } 269 | 270 | if len(field.GetValidationErrors()) != 0 { 271 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 272 | } 273 | 274 | field.Value = int16(1) 275 | 276 | if !field.IsValid() { 277 | t.Errorf("Field should be valid") 278 | } 279 | 280 | if len(field.GetValidationErrors()) != 0 { 281 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 282 | } 283 | 284 | field.Value = int32(1) 285 | 286 | if !field.IsValid() { 287 | t.Errorf("Field should be valid") 288 | } 289 | 290 | if len(field.GetValidationErrors()) != 0 { 291 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 292 | } 293 | 294 | field.Value = int64(1) 295 | 296 | if !field.IsValid() { 297 | t.Errorf("Field should be valid") 298 | } 299 | 300 | if len(field.GetValidationErrors()) != 0 { 301 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 302 | } 303 | 304 | field.Value = float32(1) 305 | 306 | if !field.IsValid() { 307 | t.Errorf("Field should be valid") 308 | } 309 | 310 | if len(field.GetValidationErrors()) != 0 { 311 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 312 | } 313 | 314 | field.Value = time.Now() 315 | 316 | if !field.IsValid() { 317 | t.Errorf("Field should be valid") 318 | } 319 | 320 | if len(field.GetValidationErrors()) != 0 { 321 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 322 | } 323 | 324 | } 325 | 326 | func TestField_Currency(t *testing.T) { 327 | field := getBasicField() 328 | field.CurrencyCode = "COP" 329 | field.Value = 43000 330 | 331 | if !field.IsValid() { 332 | t.Errorf("Field should be valid. Reason: %v", field.GetValidationErrors()) 333 | } 334 | 335 | if len(field.GetValidationErrors()) != 0 { 336 | t.Errorf("Field should have no errors. Have %v", len(field.GetValidationErrors())) 337 | } 338 | } 339 | 340 | func TestField_CurrencyValues(t *testing.T) { 341 | field := getBasicField() 342 | field.CurrencyCode = "USD" 343 | field.Value = 1 344 | 345 | if !field.IsValid() { 346 | t.Errorf("Field should be valid") 347 | } 348 | 349 | if len(field.GetValidationErrors()) != 0 { 350 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 351 | } 352 | 353 | field.Value = 1.0 354 | 355 | if !field.IsValid() { 356 | t.Errorf("Field should be valid") 357 | } 358 | 359 | if len(field.GetValidationErrors()) != 0 { 360 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 361 | } 362 | 363 | field.Value = int8(1) 364 | 365 | if !field.IsValid() { 366 | t.Errorf("Field should be valid") 367 | } 368 | 369 | if len(field.GetValidationErrors()) != 0 { 370 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 371 | } 372 | 373 | field.Value = int8(1) 374 | 375 | if !field.IsValid() { 376 | t.Errorf("Field should be valid") 377 | } 378 | 379 | if len(field.GetValidationErrors()) != 0 { 380 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 381 | } 382 | 383 | field.Value = int16(1) 384 | 385 | if !field.IsValid() { 386 | t.Errorf("Field should be valid") 387 | } 388 | 389 | if len(field.GetValidationErrors()) != 0 { 390 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 391 | } 392 | 393 | field.Value = int32(1) 394 | 395 | if !field.IsValid() { 396 | t.Errorf("Field should be valid") 397 | } 398 | 399 | if len(field.GetValidationErrors()) != 0 { 400 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 401 | } 402 | 403 | field.Value = int64(1) 404 | 405 | if !field.IsValid() { 406 | t.Errorf("Field should be valid") 407 | } 408 | 409 | if len(field.GetValidationErrors()) != 0 { 410 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 411 | } 412 | 413 | field.Value = float32(1) 414 | 415 | if !field.IsValid() { 416 | t.Errorf("Field should be valid") 417 | } 418 | 419 | if len(field.GetValidationErrors()) != 0 { 420 | t.Errorf("Field should not have errors. Have %v", len(field.GetValidationErrors())) 421 | } 422 | 423 | } 424 | 425 | func TestField_CurrencyAndNumberFormat(t *testing.T) { 426 | field := getBasicField() 427 | field.CurrencyCode = "COP" 428 | field.Value = 1 429 | field.NumberStyle = NumberStyleDecimal 430 | 431 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 432 | if field.IsValid() { 433 | t.Errorf("Field should be invalid") 434 | } 435 | 436 | if len(field.GetValidationErrors()) != 1 { 437 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 438 | } 439 | } 440 | 441 | func TestField_CurrencyAndDateFormat(t *testing.T) { 442 | field := getBasicField() 443 | field.CurrencyCode = "COP" 444 | field.DateStyle = DateStyleFull 445 | field.Value = 1 446 | 447 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 448 | if field.IsValid() { 449 | t.Errorf("Field should be invalid") 450 | } 451 | 452 | if len(field.GetValidationErrors()) != 1 { 453 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 454 | } 455 | } 456 | 457 | func TestField_CurrencyAndTimeFormat(t *testing.T) { 458 | field := getBasicField() 459 | field.CurrencyCode = "COP" 460 | field.TimeStyle = DateStyleFull 461 | field.Value = 1 462 | 463 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 464 | if field.IsValid() { 465 | t.Errorf("Field should be invalid") 466 | } 467 | 468 | if len(field.GetValidationErrors()) != 1 { 469 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 470 | } 471 | } 472 | 473 | func TestField_CurrencyAndNotNumber(t *testing.T) { 474 | field := getBasicField() 475 | field.CurrencyCode = "COP" 476 | field.Value = "1.2321" 477 | 478 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 479 | if field.IsValid() { 480 | t.Errorf("Field should be invalid") 481 | } 482 | 483 | if len(field.GetValidationErrors()) != 1 { 484 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 485 | } 486 | } 487 | 488 | func TestField_NumberAndDateStyleSet(t *testing.T) { 489 | field := getBasicField() 490 | field.NumberStyle = NumberStyleDecimal 491 | field.DateStyle = DateStyleFull 492 | field.Value = "string" 493 | 494 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 495 | if field.IsValid() { 496 | t.Errorf("Field should be invalid") 497 | } 498 | 499 | if len(field.GetValidationErrors()) != 1 { 500 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 501 | } 502 | } 503 | 504 | func TestField_NoKey(t *testing.T) { 505 | field := getBasicField() 506 | field.Value = "Value" 507 | field.Key = "" 508 | 509 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 510 | if field.IsValid() { 511 | t.Errorf("Field should be invalid") 512 | } 513 | 514 | if len(field.GetValidationErrors()) != 1 { 515 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 516 | } 517 | } 518 | 519 | func TestField_NoValue(t *testing.T) { 520 | field := getBasicField() 521 | field.Value = nil 522 | 523 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 524 | if field.IsValid() { 525 | t.Errorf("Field should be invalid") 526 | } 527 | 528 | if len(field.GetValidationErrors()) != 1 { 529 | t.Errorf("Field should have one error %v", len(field.GetValidationErrors())) 530 | } 531 | } 532 | 533 | func TestField_InvalidValueType(t *testing.T) { 534 | field := getBasicField() 535 | field.Value = false 536 | 537 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 538 | if field.IsValid() { 539 | t.Errorf("Field should be invalid") 540 | } 541 | 542 | if len(field.GetValidationErrors()) != 1 { 543 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 544 | } 545 | } 546 | 547 | func TestField_InvalidChangeMessage(t *testing.T) { 548 | field := getBasicField() 549 | field.ChangeMessage = "Fake" 550 | field.Value = "Test" 551 | 552 | t.Logf("%d, %v", len(field.GetValidationErrors()), field.GetValidationErrors()) 553 | if field.IsValid() { 554 | t.Errorf("Field should be invalid") 555 | } 556 | 557 | if len(field.GetValidationErrors()) != 1 { 558 | t.Errorf("Field should have one error. Have: %v", len(field.GetValidationErrors())) 559 | } 560 | } 561 | 562 | func getBasicLocation() Location { 563 | return Location{ 564 | Longitude: 1.0, 565 | Latitude: 1.2, 566 | Altitude: 1.3, 567 | RelevantText: "text", 568 | } 569 | } 570 | 571 | func TestLocation_GetSet(t *testing.T) { 572 | location := getBasicLocation() 573 | 574 | t.Logf("%d, %v", len(location.GetValidationErrors()), location.GetValidationErrors()) 575 | if !location.IsValid() { 576 | t.Errorf("Location should be valid. Reason: %v", location.GetValidationErrors()) 577 | } 578 | 579 | if len(location.GetValidationErrors()) != 0 { 580 | t.Errorf("Location should have no errors. Have: %v", len(location.GetValidationErrors())) 581 | } 582 | } 583 | 584 | func getBasicPass() Pass { 585 | exp := time.Now() 586 | f := getBasicField() 587 | f.Value = "string" 588 | 589 | return Pass{ 590 | FormatVersion: 1, 591 | OrganizationName: "Org", 592 | Description: "test", 593 | AppLaunchURL: "app://open", 594 | MaxDistance: 99999, 595 | Voided: false, 596 | UserInfo: map[string]interface{}{"name": "John Doe"}, 597 | ExpirationDate: &exp, 598 | Barcodes: []Barcode{getBasicBarcode()}, 599 | SerialNumber: "1234", 600 | PassTypeIdentifier: "test", 601 | TeamIdentifier: "TEAM1", 602 | AuthenticationToken: "asldadilno21o31n41lkasndio123", 603 | Generic: &GenericPass{PrimaryFields: []Field{f}}, 604 | AssociatedStoreIdentifiers: []int64{123}, 605 | } 606 | } 607 | 608 | func TestPass_GetSet(t *testing.T) { 609 | pass := getBasicPass() 610 | pass.GroupingIdentifier = "" 611 | 612 | if !pass.IsValid() { 613 | t.Errorf("Pass should be valid. Reason: %v", pass.GetValidationErrors()) 614 | } 615 | 616 | if len(pass.GetValidationErrors()) != 0 { 617 | t.Errorf("Pass should have no errors. Have: %v", len(pass.GetValidationErrors())) 618 | } 619 | } 620 | 621 | func TestPass_JSON(t *testing.T) { 622 | prs := getBasicPass() 623 | _, err := prs.toJSON() 624 | 625 | if err != nil { 626 | t.Errorf("Error marshalling json. %v", err) 627 | } 628 | } 629 | 630 | func TestPass_MissingSerial(t *testing.T) { 631 | pass := getBasicPass() 632 | pass.SerialNumber = "" 633 | 634 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 635 | if pass.IsValid() { 636 | t.Errorf("Pass should be invalid") 637 | } 638 | 639 | if len(pass.GetValidationErrors()) != 1 { 640 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 641 | } 642 | } 643 | 644 | func TestPass_MissingPassTypeId(t *testing.T) { 645 | pass := getBasicPass() 646 | pass.PassTypeIdentifier = "" 647 | 648 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 649 | if pass.IsValid() { 650 | t.Errorf("Pass should be invalid") 651 | } 652 | if len(pass.GetValidationErrors()) != 1 { 653 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 654 | } 655 | } 656 | 657 | func TestPass_MissingTeamID(t *testing.T) { 658 | pass := getBasicPass() 659 | pass.TeamIdentifier = "" 660 | 661 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 662 | if pass.IsValid() { 663 | t.Errorf("Pass should be invalid") 664 | } 665 | 666 | if len(pass.GetValidationErrors()) != 1 { 667 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 668 | } 669 | } 670 | 671 | func TestPass_MissingDescription(t *testing.T) { 672 | pass := getBasicPass() 673 | pass.Description = "" 674 | 675 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 676 | if pass.IsValid() { 677 | t.Errorf("Pass should be invalid") 678 | } 679 | 680 | if len(pass.GetValidationErrors()) != 1 { 681 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 682 | } 683 | } 684 | 685 | func TestPass_MissingFormat(t *testing.T) { 686 | pass := getBasicPass() 687 | pass.FormatVersion = 0 688 | 689 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 690 | if pass.IsValid() { 691 | t.Errorf("Pass should be invalid") 692 | } 693 | 694 | if len(pass.GetValidationErrors()) != 1 { 695 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 696 | } 697 | } 698 | 699 | func TestPass_MissingOrganization(t *testing.T) { 700 | pass := getBasicPass() 701 | pass.OrganizationName = "" 702 | 703 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 704 | if pass.IsValid() { 705 | t.Errorf("Pass should be invalid") 706 | } 707 | 708 | if len(pass.GetValidationErrors()) != 1 { 709 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 710 | } 711 | } 712 | 713 | func TestPass_NoPass(t *testing.T) { 714 | pass := getBasicPass() 715 | pass.Generic = nil 716 | pass.Coupon = nil 717 | pass.BoardingPass = nil 718 | pass.EventTicket = nil 719 | pass.StoreCard = nil 720 | 721 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 722 | if pass.IsValid() { 723 | t.Errorf("Pass should be invalid") 724 | } 725 | 726 | if len(pass.GetValidationErrors()) != 1 { 727 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 728 | } 729 | } 730 | 731 | func TestPass_InvalidAuthToken(t *testing.T) { 732 | pass := getBasicPass() 733 | pass.AuthenticationToken = "" 734 | 735 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 736 | if pass.IsValid() { 737 | t.Errorf("Pass should be invalid") 738 | } 739 | 740 | if len(pass.GetValidationErrors()) != 1 { 741 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 742 | } 743 | } 744 | 745 | func TestPass_MultiplePass(t *testing.T) { 746 | pass := getBasicPass() 747 | f := getBasicField() 748 | f.Value = "test" 749 | 750 | pass.Coupon = &Coupon{GenericPass: &GenericPass{HeaderFields: []Field{f}}} 751 | pass.BoardingPass = &BoardingPass{ 752 | GenericPass: &GenericPass{HeaderFields: []Field{f}}, 753 | TransitType: TransitTypeAir, 754 | } 755 | pass.EventTicket = &EventTicket{GenericPass: &GenericPass{HeaderFields: []Field{f}}} 756 | pass.StoreCard = &StoreCard{GenericPass: &GenericPass{HeaderFields: []Field{f}}} 757 | 758 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 759 | if pass.IsValid() { 760 | t.Errorf("Pass should be invalid") 761 | } 762 | 763 | if len(pass.GetValidationErrors()) != 1 { 764 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 765 | } 766 | } 767 | 768 | func TestPass_InvalidGeneric(t *testing.T) { 769 | pass := getBasicPass() 770 | pass.Generic = &GenericPass{HeaderFields: []Field{getBasicField()}} 771 | pass.Coupon = nil 772 | pass.BoardingPass = nil 773 | pass.EventTicket = nil 774 | pass.StoreCard = nil 775 | 776 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.Generic.GetValidationErrors()) 777 | if pass.IsValid() { 778 | t.Errorf("Pass should be invalid") 779 | } 780 | 781 | if len(pass.GetValidationErrors()) != 1 { 782 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 783 | } 784 | } 785 | 786 | func TestPass_InvalidBoarding(t *testing.T) { 787 | pass := getBasicPass() 788 | pass.Generic = nil 789 | pass.Coupon = nil 790 | pass.EventTicket = nil 791 | pass.StoreCard = nil 792 | pass.BoardingPass = &BoardingPass{ 793 | GenericPass: &GenericPass{HeaderFields: []Field{getBasicField()}}, 794 | TransitType: TransitTypeAir, 795 | } 796 | 797 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 798 | if pass.IsValid() { 799 | t.Errorf("Pass should be invalid") 800 | } 801 | 802 | if len(pass.GetValidationErrors()) != 1 { 803 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 804 | } 805 | } 806 | 807 | func TestPass_InvalidStoreCard(t *testing.T) { 808 | pass := getBasicPass() 809 | pass.Generic = nil 810 | pass.Coupon = nil 811 | pass.BoardingPass = nil 812 | pass.EventTicket = nil 813 | pass.StoreCard = &StoreCard{GenericPass: &GenericPass{HeaderFields: []Field{getBasicField()}}} 814 | 815 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 816 | if pass.IsValid() { 817 | t.Errorf("Pass should be invalid") 818 | } 819 | 820 | if len(pass.GetValidationErrors()) != 1 { 821 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 822 | } 823 | } 824 | 825 | func TestPass_InvalidEventTicket(t *testing.T) { 826 | pass := getBasicPass() 827 | pass.Generic = nil 828 | pass.Coupon = nil 829 | pass.BoardingPass = nil 830 | pass.StoreCard = nil 831 | pass.EventTicket = &EventTicket{GenericPass: &GenericPass{HeaderFields: []Field{getBasicField()}}} 832 | 833 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 834 | if pass.IsValid() { 835 | t.Errorf("Pass should be invalid") 836 | } 837 | 838 | if len(pass.GetValidationErrors()) != 1 { 839 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 840 | } 841 | } 842 | 843 | func TestPass_InvalidCoupon(t *testing.T) { 844 | pass := getBasicPass() 845 | pass.Generic = nil 846 | pass.Coupon = &Coupon{GenericPass: &GenericPass{HeaderFields: []Field{getBasicField()}}} 847 | pass.BoardingPass = nil 848 | pass.StoreCard = nil 849 | pass.EventTicket = nil 850 | 851 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 852 | if pass.IsValid() { 853 | t.Errorf("Pass should be invalid") 854 | } 855 | 856 | if len(pass.GetValidationErrors()) != 1 { 857 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 858 | } 859 | } 860 | 861 | func TestPass_NoStoreId(t *testing.T) { 862 | pass := getBasicPass() 863 | pass.AssociatedStoreIdentifiers = []int64{} 864 | 865 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 866 | if pass.IsValid() { 867 | t.Errorf("Pass should be invalid") 868 | } 869 | 870 | if len(pass.GetValidationErrors()) != 1 { 871 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 872 | } 873 | } 874 | 875 | func TestPass_InvalidGroupIdentifier(t *testing.T) { 876 | pass := getBasicPass() 877 | pass.GroupingIdentifier = "213131" 878 | 879 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 880 | if pass.IsValid() { 881 | t.Errorf("Pass should be invalid") 882 | } 883 | 884 | if len(pass.GetValidationErrors()) != 1 { 885 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 886 | } 887 | } 888 | 889 | func TestPass_SetForegroundColorHex(t *testing.T) { 890 | pass := getBasicPass() 891 | _ = pass.SetForegroundColorHex("#000000") 892 | 893 | if pass.ForegroundColor != "rgb(0,0,0)" { 894 | t.Errorf("Foreground color invalid") 895 | } 896 | } 897 | 898 | func TestPass_SetInvalidForegroundColorHex(t *testing.T) { 899 | pass := getBasicPass() 900 | err := pass.SetForegroundColorHex("#PPPPPP") 901 | 902 | t.Logf("Hex error: %v", err) 903 | if err == nil { 904 | t.Errorf("Hex parse should fail") 905 | } 906 | } 907 | 908 | func TestPass_SetBackgroundColorHex(t *testing.T) { 909 | pass := getBasicPass() 910 | _ = pass.SetBackgroundColorHex("#000000") 911 | 912 | if pass.BackgroundColor != "rgb(0,0,0)" { 913 | t.Errorf("Background color invalid") 914 | } 915 | } 916 | 917 | func TestPass_SetInvalidBackgroundColorHex(t *testing.T) { 918 | pass := getBasicPass() 919 | err := pass.SetBackgroundColorHex("#PPPPPP") 920 | 921 | t.Logf("Hex error: %v", err) 922 | if err == nil { 923 | t.Errorf("Hex parse should fail") 924 | } 925 | } 926 | 927 | func TestPass_SetLabelColorHex(t *testing.T) { 928 | pass := getBasicPass() 929 | _ = pass.SetLabelColorHex("#000000") 930 | 931 | if pass.LabelColor != "rgb(0,0,0)" { 932 | t.Errorf("Label color invalid") 933 | } 934 | } 935 | 936 | func TestPass_SetInvalidLabelColorHex(t *testing.T) { 937 | pass := getBasicPass() 938 | err := pass.SetLabelColorHex("#PPPPPP") 939 | 940 | t.Logf("Hex error: %v", err) 941 | if err == nil { 942 | t.Errorf("Hex parse should fail") 943 | } 944 | } 945 | 946 | func TestPass_SetForegroundColorRGB(t *testing.T) { 947 | pass := getBasicPass() 948 | _ = pass.SetForegroundColorRGB(0, 0, 0) 949 | 950 | if pass.ForegroundColor != "rgb(0,0,0)" { 951 | t.Errorf("Foreground color invalid") 952 | } 953 | } 954 | 955 | func TestPass_SetBackgroundColorRGB(t *testing.T) { 956 | pass := getBasicPass() 957 | _ = pass.SetBackgroundColorRGB(0, 0, 0) 958 | 959 | if pass.BackgroundColor != "rgb(0,0,0)" { 960 | t.Errorf("Foreground color invalid") 961 | } 962 | } 963 | 964 | func TestPass_SetLabelColorRGB(t *testing.T) { 965 | pass := getBasicPass() 966 | _ = pass.SetLabelColorRGB(0, 0, 0) 967 | 968 | if pass.LabelColor != "rgb(0,0,0)" { 969 | t.Errorf("Foreground color invalid") 970 | } 971 | } 972 | 973 | func TestBoardingPass_NoTransitType(t *testing.T) { 974 | pass := getBasicPass() 975 | f := getBasicField() 976 | f.Value = "1" 977 | 978 | pass.Generic = nil 979 | pass.Coupon = nil 980 | pass.EventTicket = nil 981 | pass.StoreCard = nil 982 | pass.BoardingPass = &BoardingPass{ 983 | GenericPass: &GenericPass{HeaderFields: []Field{f}}, 984 | } 985 | 986 | t.Logf("%d, %v", len(pass.GetValidationErrors()), pass.GetValidationErrors()) 987 | if pass.IsValid() { 988 | t.Errorf("Pass should be invalid") 989 | } 990 | 991 | if len(pass.GetValidationErrors()) != 1 { 992 | t.Errorf("Pass should have one errors. Have: %v", len(pass.GetValidationErrors())) 993 | } 994 | } 995 | 996 | func TestPWAssociatedApp_GetSet(t *testing.T) { 997 | pw := PWAssociatedApp{} 998 | 999 | if !pw.IsValid() { 1000 | t.Errorf("PWAssociatedApp should be valid. Reason: %v", pw.GetValidationErrors()) 1001 | } 1002 | 1003 | if len(pw.GetValidationErrors()) != 0 { 1004 | t.Errorf("PWAssociatedApp should have no errors. Have: %v", len(pw.GetValidationErrors())) 1005 | } 1006 | } 1007 | -------------------------------------------------------------------------------- /passkit.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /semantics.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import "time" 4 | 5 | // SemanticTag Representation of https://developer.apple.com/documentation/walletpasses/semantictags 6 | type SemanticTag struct { 7 | AdditionalTicketAttributes string `json:"additionalTicketAttributes,omitempty"` 8 | AdmissionLevel string `json:"admissionLevel,omitempty"` 9 | AdmissionLevelAbbreviation string `json:"admissionLevelAbbreviation,omitempty"` 10 | AlbumIDs []string `json:"albumIDs,omitempty"` 11 | AirlineCode string `json:"airlineCode,omitempty"` 12 | ArtistIds []string `json:"artistIDs,omitempty"` 13 | AttendeeName string `json:"attendeeName,omitempty"` 14 | AwayTeamAbbreviation string `json:"awayTeamAbbreviation,omitempty"` 15 | AwayTeamLocation string `json:"awayTeamLocation,omitempty"` 16 | AwayTeamName string `json:"awayTeamName,omitempty"` 17 | Balance *SemanticTagCurrencyAmount `json:"balance,omitempty"` 18 | BoardingGroup string `json:"boardingGroup,omitempty"` 19 | BoardingSequenceNumber string `json:"boardingSequenceNumber,omitempty"` 20 | CarNumber string `json:"carNumber,omitempty"` 21 | ConfirmationNumber string `json:"confirmationNumber,omitempty"` 22 | CurrentArrivalDate *time.Time `json:"currentArrivalDate,omitempty"` 23 | CurrentBoardingDate *time.Time `json:"currentBoardingDate,omitempty"` 24 | CurrentDepartureDate *time.Time `json:"currentDepartureDate,omitempty"` 25 | DepartureAirportCode string `json:"departureAirportCode,omitempty"` 26 | DepartureAirportName string `json:"departureAirportName,omitempty"` 27 | DepartureGate string `json:"departureGate,omitempty"` 28 | DepartureLocation *SemanticTagLocation `json:"departureLocation,omitempty"` 29 | DepartureLocationDescription string `json:"departureLocationDescription,omitempty"` 30 | DeparturePlatform string `json:"departurePlatform,omitempty"` 31 | DepartureStationName string `json:"departureStationName,omitempty"` 32 | DepartureTerminal string `json:"departureTerminal,omitempty"` 33 | DestinationAirportCode string `json:"destinationAirportCode,omitempty"` 34 | DestinationAirportName string `json:"destinationAirportName,omitempty"` 35 | DestinationGate string `json:"destinationGate,omitempty"` 36 | DestinationLocation *SemanticTagLocation `json:"destinationLocation,omitempty"` 37 | DestinationLocationDescription string `json:"destinationLocationDescription,omitempty"` 38 | DestinationPlatform string `json:"destinationPlatform,omitempty"` 39 | DestinationStationName string `json:"destinationStationName,omitempty"` 40 | DestinationTerminal string `json:"destinationTerminal,omitempty"` 41 | Duration *uint64 `json:"duration,omitempty"` 42 | EventEndDate *time.Time `json:"eventEndDate,omitempty"` 43 | EventLiveMessage string `json:"eventLiveMessage,omitempty"` 44 | EventName string `json:"eventName,omitempty"` 45 | EventStartDate *time.Time `json:"eventStartDate,omitempty"` 46 | EventStartDateInfo *SemanticTagEventDateInfo `json:"eventStartDateInfo,omitempty"` 47 | EventType EventType `json:"eventType,omitempty"` 48 | EntranceDescription string `json:"entranceDescription,omitempty"` 49 | FlightCode string `json:"flightCode,omitempty"` 50 | FlightNumber string `json:"flightNumber,omitempty"` 51 | Genre string `json:"genre,omitempty"` 52 | HomeTeamAbbreviation string `json:"homeTeamAbbreviation,omitempty"` 53 | HomeTeamLocation string `json:"homeTeamLocation,omitempty"` 54 | HomeTeamName string `json:"homeTeamName,omitempty"` 55 | LeagueAbbreviation string `json:"leagueAbbreviation,omitempty"` 56 | LeagueName string `json:"leagueName,omitempty"` 57 | MembershipProgramName string `json:"membershipProgramName,omitempty"` 58 | MembershipProgramNumber string `json:"membershipProgramNumber,omitempty"` 59 | OriginalArrivalDate *time.Time `json:"originalArrivalDate,omitempty"` 60 | OriginalBoardingDate *time.Time `json:"originalBoardingDate,omitempty"` 61 | OriginalDepartureDate *time.Time `json:"originalDepartureDate,omitempty"` 62 | PassengerName *SemanticTagPersonNameComponents `json:"passengerName,omitempty"` 63 | PerformerNames []string `json:"performerNames,omitempty"` 64 | PlaylistIDs []string `json:"playlistIDs,omitempty"` 65 | PriorityStatus string `json:"priorityStatus,omitempty"` 66 | Seats []SemanticTagSeat `json:"seats,omitempty"` 67 | SecurityScreening string `json:"securityScreening,omitempty"` 68 | SilenceRequested bool `json:"silenceRequested,omitempty"` 69 | SportName string `json:"sportName,omitempty"` 70 | TailgatingAllowed bool `json:"tailgatingAllowed,omitempty"` 71 | TotalPrice *SemanticTagCurrencyAmount `json:"totalPrice,omitempty"` 72 | TransitProvider string `json:"transitProvider,omitempty"` 73 | TransitStatus string `json:"transitStatus,omitempty"` 74 | TransitStatusReason string `json:"transitStatusReason,omitempty"` 75 | VehicleName string `json:"vehicleName,omitempty"` 76 | VehicleNumber string `json:"vehicleNumber,omitempty"` 77 | VehicleType string `json:"vehicleType,omitempty"` 78 | VenueBoxOfficeOpenDate *time.Time `json:"venueBoxOfficeOpenDate,omitempty"` 79 | VenueCloseDate *time.Time `json:"venueCloseDate,omitempty"` 80 | VenueDoorsOpenDate *time.Time `json:"venueDoorsOpenDate,omitempty"` 81 | VenueEntrance string `json:"venueEntrance,omitempty"` 82 | VenueEntranceDoor string `json:"venueEntranceDoor,omitempty"` 83 | VenueEntranceGate string `json:"venueEntranceGate,omitempty"` 84 | VenueEntrancePortal string `json:"venueEntrancePortal,omitempty"` 85 | VenueFanZoneOpenDate *time.Time `json:"venueFanZoneOpenDate,omitempty"` 86 | VenueGatesOpenDate *time.Time `json:"venueGatesOpenDate,omitempty"` 87 | VenueLocation *SemanticTagLocation `json:"venueLocation,omitempty"` 88 | VenueName string `json:"venueName,omitempty"` 89 | VenueOpenDate *time.Time `json:"venueOpenDate,omitempty"` 90 | VenueParkingLotsOpenDate *time.Time `json:"venueParkingLotsOpenDate,omitempty"` 91 | VenuePhoneNumber string `json:"venuePhoneNumber,omitempty"` 92 | VenueRegionName string `json:"venueRegionName,omitempty"` 93 | VenueRoom string `json:"venueRoom,omitempty"` 94 | WifiAccess []SemanticTagWifiNetwork `json:"wifiAccess,omitempty"` 95 | } 96 | 97 | func (s *SemanticTag) IsValid() bool { 98 | return len(s.GetValidationErrors()) == 0 99 | } 100 | 101 | func (s *SemanticTag) GetValidationErrors() []string { 102 | var validationErrors []string 103 | // Only validate what is validatable 104 | if s.WifiAccess != nil { 105 | for _, wifiAccess := range s.WifiAccess { 106 | if !wifiAccess.IsValid() { 107 | validationErrors = append(validationErrors, wifiAccess.GetValidationErrors()...) 108 | } 109 | } 110 | } 111 | return validationErrors 112 | } 113 | 114 | // SemanticTagEventDateInfo Representation of https://developer.apple.com/documentation/walletpasses/semantictagtype/eventdateinfo-data.dictionary 115 | type SemanticTagEventDateInfo struct { 116 | DateDescription string `json:"dateDescription,omitempty"` 117 | IsTentative bool `json:"isTentative,omitempty"` 118 | OriginalDate *time.Time `json:"originalDate,omitempty"` 119 | } 120 | 121 | // SemanticTagCurrencyAmount Representation of https://developer.apple.com/documentation/walletpasses/semantictagtype/currencyamount 122 | type SemanticTagCurrencyAmount struct { 123 | Amount string `json:"amount"` 124 | CurrencyCode string `json:"currencyCode"` 125 | } 126 | 127 | func (s *SemanticTagCurrencyAmount) IsValid() bool { 128 | return len(s.GetValidationErrors()) == 0 129 | } 130 | 131 | func (s *SemanticTagCurrencyAmount) GetValidationErrors() []string { 132 | return []string{} 133 | } 134 | 135 | // SemanticTagLocation Representation of https://developer.apple.com/documentation/walletpasses/semantictagtype/location 136 | type SemanticTagLocation struct { 137 | Latitude float64 `json:"latitude"` 138 | Longitude float64 `json:"longitude"` 139 | } 140 | 141 | func (l *SemanticTagLocation) IsValid() bool { 142 | return len(l.GetValidationErrors()) == 0 143 | } 144 | 145 | func (l *SemanticTagLocation) GetValidationErrors() []string { 146 | return []string{} 147 | } 148 | 149 | // SemanticTagPersonNameComponents Representation of https://developer.apple.com/documentation/walletpasses/semantictagtype/personnamecomponents 150 | type SemanticTagPersonNameComponents struct { 151 | FamilyName string `json:"familyName,omitempty"` 152 | GivenName string `json:"givenName,omitempty"` 153 | MiddleName string `json:"middleName,omitempty"` 154 | NamePrefix string `json:"namePrefix,omitempty"` 155 | NameSuffix string `json:"nameSuffix,omitempty"` 156 | Nickname string `json:"nickname,omitempty"` 157 | PhoneticRepresentation string `json:"phoneticRepresentation,omitempty"` 158 | } 159 | 160 | func (l *SemanticTagPersonNameComponents) IsValid() bool { 161 | return len(l.GetValidationErrors()) == 0 162 | } 163 | 164 | func (l *SemanticTagPersonNameComponents) GetValidationErrors() []string { 165 | return []string{} 166 | } 167 | 168 | // SemanticTagSeat Representation of https://developer.apple.com/documentation/walletpasses/semantictagtype/seat 169 | type SemanticTagSeat struct { 170 | SeatDescription string `json:"seatDescription,omitempty"` 171 | SeatIdentifier string `json:"seatIdentifier,omitempty"` 172 | SeatNumber string `json:"seatNumber,omitempty"` 173 | SeatRow string `json:"seatRow,omitempty"` 174 | SeatSection string `json:"seatSection,omitempty"` 175 | SeatType string `json:"seatType,omitempty"` 176 | } 177 | 178 | func (l *SemanticTagSeat) IsValid() bool { 179 | return len(l.GetValidationErrors()) == 0 180 | } 181 | 182 | func (l *SemanticTagSeat) GetValidationErrors() []string { 183 | return []string{} 184 | } 185 | 186 | type SemanticTagWifiNetwork struct { 187 | SSID string `json:"ssid"` 188 | Password string `json:"password"` 189 | } 190 | 191 | func (w *SemanticTagWifiNetwork) IsValid() bool { 192 | return len(w.GetValidationErrors()) == 0 193 | } 194 | 195 | func (w *SemanticTagWifiNetwork) GetValidationErrors() []string { 196 | var validationErrors []string 197 | // Must have both attributes 198 | if w.SSID == "" || w.Password == "" { 199 | validationErrors = append(validationErrors, "SemanticTagWifiNetwork: Both ssid and password must be set") 200 | } 201 | return validationErrors 202 | } 203 | -------------------------------------------------------------------------------- /signing.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/asn1" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "time" 10 | 11 | "go.mozilla.org/pkcs7" 12 | "software.sslmate.com/src/go-pkcs12" 13 | ) 14 | 15 | const ( 16 | manifestJsonFileName = "manifest.json" 17 | passJsonFileName = "pass.json" 18 | personalizationJsonFileName = "personalization.json" 19 | signatureFileName = "signature" 20 | ) 21 | 22 | type Signer interface { 23 | CreateSignedAndZippedPassArchive(p *Pass, t PassTemplate, i *SigningInformation) ([]byte, error) 24 | CreateSignedAndZippedPersonalizedPassArchive(p *Pass, pz *Personalization, t PassTemplate, i *SigningInformation) ([]byte, error) 25 | SignManifestFile(manifestJson []byte, i *SigningInformation) ([]byte, error) 26 | } 27 | 28 | type SigningInformation struct { 29 | signingCert *x509.Certificate 30 | appleWWDRCACert *x509.Certificate 31 | privateKey interface{} 32 | } 33 | 34 | func LoadSigningInformationFromFiles(pkcs12KeyStoreFilePath, keyStorePassword, appleWWDRCAFilePath string) (*SigningInformation, error) { 35 | p12, err := os.ReadFile(pkcs12KeyStoreFilePath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | ca, err := os.ReadFile(appleWWDRCAFilePath) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return LoadSigningInformationFromBytes(p12, keyStorePassword, ca) 46 | } 47 | 48 | func LoadSigningInformationFromBytes(pkcs12KeyStoreFile []byte, keyStorePassword string, appleWWDRCAFile []byte) (*SigningInformation, error) { 49 | info := &SigningInformation{} 50 | 51 | pk, cer, err := pkcs12.Decode(pkcs12KeyStoreFile, keyStorePassword) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | if err := verify(cer); err != nil { 57 | return nil, fmt.Errorf("error decoding pkcs12: %w", err) 58 | } 59 | 60 | wwdrca, err := x509.ParseCertificate(appleWWDRCAFile) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | if err := verify(wwdrca); err != nil { 66 | return nil, fmt.Errorf("error verifying Apple WWDRCAFile: %w", err) 67 | } 68 | 69 | info.privateKey = pk 70 | info.signingCert = cer 71 | info.appleWWDRCACert = wwdrca 72 | 73 | return info, nil 74 | } 75 | 76 | // verify checks if a certificate has expired 77 | func verify(cert *x509.Certificate) error { 78 | _, err := cert.Verify(x509.VerifyOptions{Roots: x509.NewCertPool()}) 79 | if err == nil { 80 | return nil 81 | } 82 | 83 | switch e := err.(type) { 84 | case x509.CertificateInvalidError: 85 | switch e.Reason { 86 | case x509.Expired: 87 | return errors.New("certificate has expired or is not yet valid") 88 | default: 89 | return err 90 | } 91 | case x509.UnknownAuthorityError: 92 | // Apple cert isn't in the cert pool 93 | // ignoring this error 94 | return nil 95 | default: 96 | return err 97 | } 98 | } 99 | 100 | func signManifestFile(manifestJson []byte, i *SigningInformation) ([]byte, error) { 101 | if manifestJson == nil { 102 | return nil, fmt.Errorf("manifestJson has to be present") 103 | } 104 | 105 | s, err := pkcs7.NewSignedData(manifestJson) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | s.AddCertificate(i.appleWWDRCACert) 111 | 112 | signingTimeAttr, err := createSigningTimeAttribute() 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | signerInfoConfig := pkcs7.SignerInfoConfig{ 118 | ExtraSignedAttributes: []pkcs7.Attribute{signingTimeAttr}, 119 | } 120 | 121 | err = s.AddSigner(i.signingCert, i.privateKey, signerInfoConfig) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | s.Detach() 127 | return s.Finish() 128 | } 129 | 130 | func createSigningTimeAttribute() (pkcs7.Attribute, error) { 131 | signingTimeBytes, err := asn1.Marshal(time.Now().UTC()) 132 | if err != nil { 133 | return pkcs7.Attribute{}, err 134 | } 135 | 136 | return pkcs7.Attribute{ 137 | Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}, 138 | Value: signingTimeBytes, 139 | }, nil 140 | } 141 | -------------------------------------------------------------------------------- /signing_test.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestSigner_LoadSigningInformationFromFiles(t *testing.T) { 10 | signingInfo, err := LoadSigningInformationFromFiles(filepath.Join("test", "passbook", "passkit.p12"), "password", filepath.Join("test", "passbook", "ca.pem")) 11 | if err != nil { 12 | t.Errorf("could not load signing info. %v", err) 13 | } 14 | 15 | _, err = signManifestFile(nil, signingInfo) 16 | if err == nil { 17 | t.Errorf("should fail") 18 | } 19 | 20 | passJson, err := os.ReadFile(filepath.Join("test", "pass2.json")) 21 | if err != nil { 22 | t.Errorf("could not load pass json file. %v", err) 23 | } 24 | 25 | _, err = signManifestFile(passJson, signingInfo) 26 | if err != nil { 27 | t.Errorf("could not sign manifest. %v", err) 28 | } 29 | } 30 | 31 | func TestSigner_LoadSigningInformationFromFilesPaths(t *testing.T) { 32 | _, err := LoadSigningInformationFromFiles(filepath.Join("test", "passbook", "xxxx"), "xxxxx", filepath.Join("test", "passbook", "AppleWWDRCA.cer")) 33 | if err == nil { 34 | t.Errorf("loading cert should fail.") 35 | } 36 | 37 | _, err = LoadSigningInformationFromFiles(filepath.Join("test", "passbook", "passkit.p12"), "xxxxx", filepath.Join("test", "passbook", "xxxx.cer")) 38 | if err == nil { 39 | t.Errorf("loading cert should fail.") 40 | } 41 | } 42 | 43 | func TestSigner_ValidCerts(t *testing.T) { 44 | _, err := LoadSigningInformationFromFiles(filepath.Join("test", "passbook", "passkit.p12"), "password", filepath.Join("test", "passbook", "ca-chain.cert.pem")) 45 | if err == nil { 46 | t.Errorf("should fail") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | BundleIconRetinaHD = "icon@3x.png" 15 | BundleIconRetina = "icon@2x.png" 16 | BundleIcon = "icon.png" 17 | BundleLogoRetinaHD = "logo@3x.png" 18 | BundleLogoRetina = "logo@2x.png" 19 | BundleLogo = "logo.png" 20 | BundleThumbnailRetinaHD = "thumbnail@3x.png" 21 | BundleThumbnailRetina = "thumbnail@2x.png" 22 | BundleThumbnail = "thumbnail.png" 23 | BundleStripRetinaHD = "strip@3x.png" 24 | BundleStripRetina = "strip@2x.png" 25 | BundleStrip = "strip.png" 26 | BundleBackgroundRetinaHD = "background@3x.png" 27 | BundleBackgroundRetina = "background@2x.png" 28 | BundleBackground = "background.png" 29 | BundleFooterRetinaHD = "footer@3x.png" 30 | BundleFooterRetina = "footer@2x.png" 31 | BundleFooter = "footer.png" 32 | BundlePersonalizationLogoRetinaHD = "personalizationLogo@3x.png" 33 | BundlePersonalizationLogoRetina = "personalizationLogo@2x.png" 34 | BundlePersonalizationLogo = "personalizationLogo.png" 35 | ) 36 | 37 | type PassTemplate interface { 38 | ProvisionPassAtDirectory(tmpDirPath string) error 39 | GetAllFiles() (map[string][]byte, error) 40 | } 41 | 42 | type FolderPassTemplate struct { 43 | templateDir string 44 | } 45 | 46 | func NewFolderPassTemplate(templateDir string) *FolderPassTemplate { 47 | return &FolderPassTemplate{templateDir: templateDir} 48 | } 49 | 50 | func (f *FolderPassTemplate) ProvisionPassAtDirectory(tmpDirPath string) error { 51 | return copyDir(f.templateDir, tmpDirPath) 52 | } 53 | 54 | func (f *FolderPassTemplate) GetAllFiles() (map[string][]byte, error) { 55 | loaded, err := loadDir(f.templateDir) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | ret := make(map[string][]byte) 61 | for name, data := range loaded { 62 | ret[filepath.Base(name)] = data 63 | } 64 | 65 | return ret, err 66 | } 67 | 68 | type InMemoryPassTemplate struct { 69 | files map[string][]byte 70 | } 71 | 72 | func NewInMemoryPassTemplate() *InMemoryPassTemplate { 73 | return &InMemoryPassTemplate{files: make(map[string][]byte)} 74 | } 75 | 76 | func (m *InMemoryPassTemplate) ProvisionPassAtDirectory(tmpDirPath string) error { 77 | dst := filepath.Clean(tmpDirPath) 78 | 79 | _, err := os.Stat(dst) 80 | if err != nil && !os.IsNotExist(err) { 81 | return err 82 | } 83 | 84 | err = os.MkdirAll(dst, os.ModeDir) 85 | if err != nil { 86 | return nil 87 | } 88 | 89 | for file, d := range m.files { 90 | err = os.WriteFile(filepath.Join(dst, string(file)), d, 0644) 91 | if err != nil { 92 | _ = os.RemoveAll(dst) 93 | return err 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (m *InMemoryPassTemplate) GetAllFiles() (map[string][]byte, error) { 101 | return m.files, nil 102 | } 103 | 104 | func (m *InMemoryPassTemplate) AddFileBytes(name string, data []byte) { 105 | m.files[name] = data 106 | } 107 | 108 | func (m *InMemoryPassTemplate) AddFileBytesLocalized(name, locale string, data []byte) { 109 | m.files[m.pathForLocale(name, locale)] = data 110 | } 111 | 112 | func (m *InMemoryPassTemplate) downloadFile(u url.URL) ([]byte, error) { 113 | timeout := 10 * time.Second 114 | client := http.Client{ 115 | Timeout: timeout, 116 | } 117 | 118 | response, err := client.Get(u.String()) 119 | if err != nil { 120 | return nil, err 121 | } 122 | defer func(Body io.ReadCloser) { 123 | _ = Body.Close() 124 | }(response.Body) 125 | 126 | b, err := io.ReadAll(response.Body) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | return b, nil 132 | } 133 | 134 | func (m *InMemoryPassTemplate) AddFileFromURL(name string, u url.URL) error { 135 | b, err := m.downloadFile(u) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | m.files[name] = b 141 | return nil 142 | } 143 | 144 | func (m *InMemoryPassTemplate) AddFileFromURLLocalized(name, locale string, u url.URL) error { 145 | b, err := m.downloadFile(u) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | m.files[m.pathForLocale(name, locale)] = b 151 | return nil 152 | } 153 | 154 | func (m *InMemoryPassTemplate) AddAllFiles(directoryWithFilesToAdd string) error { 155 | src := filepath.Clean(directoryWithFilesToAdd) 156 | loaded, err := loadDir(src) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | for name, data := range loaded { 162 | m.files[filepath.Base(name)] = data 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func (m *InMemoryPassTemplate) pathForLocale(name string, locale string) string { 169 | if strings.TrimSpace(locale) == "" { 170 | return name 171 | } 172 | 173 | return filepath.Join(locale+".lproj", name) 174 | } 175 | -------------------------------------------------------------------------------- /test/StoreCard.raw/.ignored_file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/.ignored_file -------------------------------------------------------------------------------- /test/StoreCard.raw/en.lproj/.ignored_file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/en.lproj/.ignored_file -------------------------------------------------------------------------------- /test/StoreCard.raw/en.lproj/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/en.lproj/logo.png -------------------------------------------------------------------------------- /test/StoreCard.raw/en.lproj/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/en.lproj/logo@2x.png -------------------------------------------------------------------------------- /test/StoreCard.raw/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/icon.png -------------------------------------------------------------------------------- /test/StoreCard.raw/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/icon@2x.png -------------------------------------------------------------------------------- /test/StoreCard.raw/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/logo.png -------------------------------------------------------------------------------- /test/StoreCard.raw/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/logo@2x.png -------------------------------------------------------------------------------- /test/StoreCard.raw/strip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/strip.png -------------------------------------------------------------------------------- /test/StoreCard.raw/strip@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/StoreCard.raw/strip@2x.png -------------------------------------------------------------------------------- /test/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDGDCCAgCgAwIBAgIJAPS2GZT4oi8cMA0GCSqGSIb3DQEBDQUAMBgxFjAUBgNV 3 | BAMMDVB1c2h5VGVzdFJvb3QwIBcNMTcwNDE3MDA1MzMwWhgPMjExNzAzMjQwMDUz 4 | MzBaMBgxFjAUBgNVBAMMDVB1c2h5VGVzdFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA 5 | A4IBDwAwggEKAoIBAQDF3Y3kV01ojXWbpZsww9yALoZibzZNOV91ft7DgTqFkHPz 6 | p9J35NHsFSAXF1yHgl/xRnJqP8JC8eqjg/bPE0zzF6ErEaj7zGxDYN3+v0+k6gdL 7 | s+hFWbi38h11lEwj+AejAoOihgHCHjguuhFejGsphFI0HWmR3wXsUIsowzlgmWA6 8 | SUR15gFXbktaPl+XtGxyWcwO5anAUBrwEbChPGNCeLL0fAbBUjKqfw7mhY1OFlv/ 9 | LAceM1GdEDvK6mQR4vw+aqypWCX2WPv948rVXegy7VM/lt2YFYEGogtjdrOsT4nd 10 | s/N0/gboq4ebU4mLmsFgJxv0iYU0DFXLw/8hlNHPAgMBAAGjYzBhMB0GA1UdDgQW 11 | BBSxqubF1u3WRgfvF1iaFsKwixZzszAfBgNVHSMEGDAWgBSxqubF1u3WRgfvF1ia 12 | FsKwixZzszAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG 13 | 9w0BAQ0FAAOCAQEAJ1E2peRR58svomV3gYulC67ktuByiqpJkMBs9AJBzUtQWf/v 14 | s+RUxpRqJawqvfgAtIVuIZAy0zTNkdasQOjSvTkmibQSV61kmyZx0stzwqdKdXfJ 15 | 2NbLqfWPd+UyVBKJp+oSGJe7EM/lG1OChDPid57Fn+Vm4SUnv4ly4r0P80buntbk 16 | OWkx3YeCTe6mWa55z+IH7P2Ef0j3V5ui4p7dZ1hOiU2zsIeuGb9jX0h/Tbqyt+mu 17 | 93Mgig3e+hgVhNwYEWXzen2SbxzOP2KkRvIjFTNbPuiK8eSPp8RKSME+bc6CY3ZN 18 | UzV5ZWwUxxqk6jHrxrnBt0PxFFGY5gKurTR3+w== 19 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/log4j.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Patrice Brend'amour 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # Set root logger level to DEBUG and its only appender to A1. 17 | log4j.rootLogger=DEBUG, A1 18 | # A1 is set to be a ConsoleAppender. 19 | log4j.appender.A1=org.apache.log4j.ConsoleAppender 20 | # A1 uses PatternLayout. 21 | log4j.appender.A1.layout=org.apache.log4j.PatternLayout 22 | log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n 23 | -------------------------------------------------------------------------------- /test/pass2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "passTypeIdentifier": "pti", 4 | "teamIdentifier": "ti", 5 | "barcodes": [ 6 | { 7 | "format": "PKBarcodeFormatQR", 8 | "message": "abcdefg", 9 | "messageEncoding": "UTF-8" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /test/passbook/AppleWWDRCA.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/passbook/AppleWWDRCA.cer -------------------------------------------------------------------------------- /test/passbook/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDyTCCArGgAwIBAgIJAP5gHeV6iSShMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxEDAOBgNVBAMMB1Bhc3NraXQxIjAgBgkqhkiG9w0BCQEW 5 | E25vcmVwbHlAZXhhbXBsZS5jb20wHhcNMTkwMTMxMTYzNjA5WhcNMjQwMTMwMTYz 6 | NjA5WjB7MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE 7 | CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQDDAdQYXNza2l0MSIw 8 | IAYJKoZIhvcNAQkBFhNub3JlcGx5QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B 9 | AQEFAAOCAQ8AMIIBCgKCAQEAm2NQBlSMkshHRxOJcBP1dIu/kmIJIyoh4awze961 10 | CvsjDw0xEMp/vMRnHOpJLMjcfLVhHFE/+iGsw+0oEaWxuAaWHplBnMDuIk8dZidX 11 | rcDN8dgP5Odq+GVfXKn40Kn1yQVz7+MHqR86J5NNOfi0Tet4c0lRxvFsTEvCfWEp 12 | ghOs3b5r7pEeQaR6872Ezj9uVu4fp6bYDtsV1X2iXy95c+3wWL+qi+tG+/S+ejxa 13 | /rann2o+cJEHGjtEzLhpSp0qhaF2MEs4hIwxjQorYf6Ul1ujZl/n9OvdRx7leqBs 14 | 77Rj7mdkNb7voyMeyXhldaH8l0BCVeCki2bBDXgfRairTwIDAQABo1AwTjAdBgNV 15 | HQ4EFgQULw8Cog3hkx5MHy1AVcbpDu6ovZ4wHwYDVR0jBBgwFoAULw8Cog3hkx5M 16 | Hy1AVcbpDu6ovZ4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQQes 17 | 3r/mFRFlqA46KuxcBVmVd4kAaiIESvOx39Tkh+9kCeRz8pTNINICF2jByZzglkQU 18 | f/VlbhXHJBXEcVTn+LDRlioQ7/R18aJkRjmSEaePckaPGcBhhH+C82PfxN8lg5in 19 | K8uNLrbh8N7c0XKSaYgkgzKW1fbrzmAm2VihyZI+HPx8gMRbq8+k+4ReKbDjXC0J 20 | D6A1OfT/gbaTMfp9EDrdUe2m5QCD8fbzv3J01Nn9+FQiEcT+6ywPGB1h8lZ4NnzQ 21 | z/KkLAgTp12QBwYHzINMmAtQVrd9Qej/CJBHQo83MViQLCeJhJr4QsxIQltxI9tg 22 | s//F1vXXfYL8lfe55w== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /test/passbook/passkit.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvinbaena/passkit/0fbedefeee8dde2fb9e373f06e24ae61d74faec4/test/passbook/passkit.p12 -------------------------------------------------------------------------------- /test/server-certs.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8TCCAdmgAwIBAgICbE8wDQYJKoZIhvcNAQENBQAwGDEWMBQGA1UEAwwNUHVz 3 | aHlUZXN0Um9vdDAgFw0xNzA0MTcwMDUzMzBaGA8yMTE3MDMyNDAwNTMzMFowHzEd 4 | MBsGA1UEAwwUY29tLnJlbGF5cmlkZXMucHVzaHkwggEiMA0GCSqGSIb3DQEBAQUA 5 | A4IBDwAwggEKAoIBAQDHZkZBnDKM4Gt+WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c36 6 | 41/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTkKljDGe8tuDncT1qSrp/UuikgdIAAiCXA 7 | /vClWPYqZcHAUc9/OcfRiyK5AmJdzz+UbY803ArSPHjz3+Mk6C9tnzBXzG8oJq9o 8 | EKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7U 9 | j8wRstdr2xWhPg1fdIVHzudYubJ7M/h95JQFKtwqEevtLUa4BJgi8SKvRX5NnkGE 10 | QMui1ercRuklVURTeoGDQYENiFnzTyI0J2tw3T+dAgMBAAGjPDA6MAkGA1UdEwQC 11 | MAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD 12 | ATANBgkqhkiG9w0BAQ0FAAOCAQEAnHHYMvBWglQLOUmNOalCMopmk9yKHM7+Sc9h 13 | KsTWJW+YohF5zkRhnwUFxW85Pc63rRVA0qyI5zHzRtwYlcZHU57KttJyDGe1rm/0 14 | ZUqXharurJzyI09jcwRpDY8EGktrGirE1iHDqQTHNDHyS8iMVU6aPCo0xur63G5y 15 | XzoIVhQXsBuwoU4VKb3n5CrxKEVcmE/nYF/Tk0rTtCrZF7TR3y/oxrp359goJ1b2 16 | /OjXN4dlqND41SbVTTL0FyXU3ebaS4DALA3pyVa1Rijw7vgEbFabsuMaAbdvlprn 17 | RwUjsrRVu3Tx7sp/NqmeBLVru5nH/yHStDjSdvQtI2ipNGK/9w== 18 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHZkZBnDKM4Gt+ 3 | WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c3641/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTk 4 | KljDGe8tuDncT1qSrp/UuikgdIAAiCXA/vClWPYqZcHAUc9/OcfRiyK5AmJdzz+U 5 | bY803ArSPHjz3+Mk6C9tnzBXzG8oJq9oEKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69 6 | m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7Uj8wRstdr2xWhPg1fdIVHzudYubJ7M/h9 7 | 5JQFKtwqEevtLUa4BJgi8SKvRX5NnkGEQMui1ercRuklVURTeoGDQYENiFnzTyI0 8 | J2tw3T+dAgMBAAECggEBAMOsIZWQ6ipEsDe1R+vuq9Z6XeP8nwb7C2FXaKGji0Gz 9 | 78YcCruln7KsHKkkD3UVw0Wa2Q1S8Kbf6A9fXutWL9f1yRHg7Ui0BDSE2ob2zAW5 10 | lRLnGs+nlSnV4WQQ5EY9NVDz8IcNR+o2znWhbb65kATvQuJO+l/lWWWBqbb+7rW+ 11 | RHy43p7U8cK63nXJy9eHZ7eOgGGUMUX+Yg0g47RGYxlIeSDrtPCXlNuwwAJY7Ecp 12 | LVltCAyCJEaLVwQpz61PTdmkb9HCvkwiuL6cnjtpoAdXCWX7tV61UNweNkvALIWR 13 | kMukFFE/H6JlAkcbw4na1KwQ3glWIIB2H/vZyMNdnyECgYEA78VEXo+iAQ6or4bY 14 | xUQFd/hIibIYMzq8PxDMOmD86G78u5Ho0ytetht5Xk1xmhv402FZCL1LsAEWpCBs 15 | a9LUwo30A23KaTA7Oy5oo5Md1YJejSNOCR+vs5wAo0SZov5tQaxVMoj3vZZqnJzJ 16 | 3A+XUgYZddIFkn8KJjgU/QVapTMCgYEA1OV1okYF2u5VW90RkVdvQONNcUvlKEL4 17 | UMSF3WJnORmtUL3Dt8AFt9b7pfz6WtVr0apT5SSIFA1+305PTpjjaw25m1GftL3U 18 | 5QwkmgTKxnPD/YPC6tImp+OUXHmk+iTgmQ9HaBpEplcyjD0EP2LQsIc6qiku/P2n 19 | OT8ArOkk5+8CgYEA7B98wRL6G8hv3swRVdMy/36HEPNOWcUR9Zl5RlSVO+FxCtca 20 | Tjt7viM4VuI1aer6FFDd+XlRvDaWMXOs0lKCLEbXczkACK7y5clCSzRqQQVuT9fg 21 | 1aNayKptBlxcYOPmfLJWBLpWH2KuAyV0tT61apWPJTR7QFXTjOfV44cOSXkCgYAH 22 | CvAxRg+7hlbcixuhqzrK8roFHXWfN1fvlBC5mh/AC9Fn8l8fHQMTadE5VH0TtCu0 23 | 6+WKlwLJZwjjajvFZdlgGTwinzihSgZY7WXoknAC0KGTKWCxU/Jja2vlA0Ep5T5o 24 | 0dCS6QuMVSYe7YXOcv5kWJTgPCyJwfpeMm9bSPsnkQKBgQChy4vU3J6CxGzwuvd/ 25 | 011kszao+cHn1DdMTyUhvA/O/paB+BAVktHm+o/i+kOk4OcPjhRqewzZZdf7ie5U 26 | hUC8kIraXM4aZt69ThQkAIER89wlhxsFXUmGf7ZMXm8f7pvM6/MDaMW3mEsfbL0U 27 | Y3jy0E30W5s1XCW3gmZ1Vg2xAg== 28 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package passkit 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | // CopyFile copies the contents of the file named src to the file named 12 | // by dst. The file will be created if it does not already exist. If the 13 | // destination file exists, all it's contents will be replaced by the contents 14 | // of the source file. The file mode will be copied from the source and 15 | // the copied data is synced/flushed to stable storage. 16 | func copyFile(src, dst string) (err error) { 17 | in, err := os.Open(src) 18 | if err != nil { 19 | return 20 | } 21 | //goland:noinspection ALL 22 | defer in.Close() 23 | 24 | out, err := os.Create(dst) 25 | if err != nil { 26 | return 27 | } 28 | defer func() { 29 | if e := out.Close(); e != nil { 30 | err = e 31 | } 32 | }() 33 | 34 | _, err = io.Copy(out, in) 35 | if err != nil { 36 | return 37 | } 38 | 39 | err = out.Sync() 40 | if err != nil { 41 | return 42 | } 43 | 44 | si, err := os.Stat(src) 45 | if err != nil { 46 | return 47 | } 48 | err = os.Chmod(dst, si.Mode()) 49 | if err != nil { 50 | return 51 | } 52 | 53 | return 54 | } 55 | 56 | // CopyDir recursively copies a directory tree, attempting to preserve permissions. 57 | // Source directory must exist, destination directory must *not* exist. 58 | // Symlinks are ignored and skipped. 59 | func copyDir(src string, dst string) (err error) { 60 | src = filepath.Clean(src) 61 | dst = filepath.Clean(dst) 62 | 63 | si, err := os.Stat(src) 64 | if err != nil { 65 | return err 66 | } 67 | if !si.IsDir() { 68 | return fmt.Errorf("source is not a directory") 69 | } 70 | 71 | _, err = os.Stat(dst) 72 | if err != nil && !os.IsNotExist(err) { 73 | return 74 | } 75 | 76 | if err == nil { 77 | return fmt.Errorf("destination already exists") 78 | } 79 | 80 | err = os.MkdirAll(dst, si.Mode()) 81 | if err != nil { 82 | return 83 | } 84 | 85 | entries, err := os.ReadDir(src) 86 | if err != nil { 87 | return 88 | } 89 | 90 | for _, entry := range entries { 91 | srcPath := filepath.Join(src, entry.Name()) 92 | dstPath := filepath.Join(dst, entry.Name()) 93 | 94 | if entry.IsDir() { 95 | err = copyDir(srcPath, dstPath) 96 | if err != nil { 97 | return 98 | } 99 | } else { 100 | // Skip symlinks. 101 | if entry.Type()&os.ModeSymlink != 0 { 102 | continue 103 | } 104 | 105 | err = copyFile(srcPath, dstPath) 106 | if err != nil { 107 | return 108 | } 109 | } 110 | } 111 | 112 | return 113 | } 114 | 115 | // CopyDir recursively copies a directory tree, attempting to preserve permissions. 116 | // Source directory must exist, destination directory must *not* exist. 117 | // Symlinks are ignored and skipped. 118 | func loadDir(src string) (files map[string][]byte, err error) { 119 | src = filepath.Clean(src) 120 | 121 | si, err := os.Stat(src) 122 | if err != nil { 123 | return nil, err 124 | } 125 | if !si.IsDir() { 126 | return nil, fmt.Errorf("source is not a directory") 127 | } 128 | 129 | entries, err := os.ReadDir(src) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | for _, entry := range entries { 135 | srcPath := filepath.Join(src, entry.Name()) 136 | 137 | if entry.IsDir() { 138 | files, err = loadDir(srcPath) 139 | if err != nil { 140 | return nil, err 141 | } 142 | } else { 143 | // Skip symlinks. 144 | if entry.Type()&os.ModeSymlink != 0 { 145 | continue 146 | } 147 | 148 | f, err := os.ReadFile(srcPath) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | if files == nil { 154 | files = make(map[string][]byte) 155 | } 156 | 157 | files[srcPath] = f 158 | } 159 | } 160 | 161 | return 162 | } 163 | 164 | func addFiles(w *zip.Writer, basePath, baseInZip string) error { 165 | // Open the Directory 166 | files, err := os.ReadDir(basePath) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | for _, file := range files { 172 | if !file.IsDir() { 173 | dat, err := os.ReadFile(basePath + file.Name()) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | // Add some files to the archive. 179 | f, err := w.Create(baseInZip + file.Name()) 180 | if err != nil { 181 | return err 182 | } 183 | _, err = f.Write(dat) 184 | if err != nil { 185 | return err 186 | } 187 | } else if file.IsDir() { 188 | 189 | // Recurse 190 | newBase := basePath + file.Name() + "/" 191 | return addFiles(w, newBase, file.Name()+"/") 192 | } 193 | } 194 | 195 | return nil 196 | } 197 | --------------------------------------------------------------------------------