├── .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 |
--------------------------------------------------------------------------------