├── .gitignore
├── .idea
├── .gitignore
└── copyright
│ ├── MIT_Crimson_Technologies__LLC.xml
│ └── profiles_settings.xml
├── LICENSE
├── README.md
├── browser_info.go
├── card_type.go
├── card_type_test.go
├── encrypt.go
├── encrypter.go
├── encrypter_test.go
├── format.go
├── format_test.go
├── go.mod
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # Generated files
13 | .idea/**/contentModel.xml
14 |
15 | # Sensitive or high-churn files
16 | .idea/**/dataSources/
17 | .idea/**/dataSources.ids
18 | .idea/**/dataSources.local.xml
19 | .idea/**/sqlDataSources.xml
20 | .idea/**/dynamic.xml
21 | .idea/**/uiDesigner.xml
22 | .idea/**/dbnavigator.xml
23 |
24 | # Gradle
25 | .idea/**/gradle.xml
26 | .idea/**/libraries
27 |
28 | # Gradle and Maven with auto-import
29 | # When using Gradle or Maven with auto-import, you should exclude module files,
30 | # since they will be recreated, and may cause churn. Uncomment if using
31 | # auto-import.
32 | .idea/artifacts
33 | .idea/compiler.xml
34 | .idea/jarRepositories.xml
35 | .idea/modules.xml
36 | .idea/*.iml
37 | .idea/modules
38 | *.iml
39 | *.ipr
40 |
41 | # CMake
42 | cmake-build-*/
43 |
44 | # Mongo Explorer plugin
45 | .idea/**/mongoSettings.xml
46 |
47 | # File-based project format
48 | *.iws
49 |
50 | # IntelliJ
51 | out/
52 |
53 | # mpeltonen/sbt-idea plugin
54 | .idea_modules/
55 |
56 | # JIRA plugin
57 | atlassian-ide-plugin.xml
58 |
59 | # Cursive Clojure plugin
60 | .idea/replstate.xml
61 |
62 | # Crashlytics plugin (for Android Studio and IntelliJ)
63 | com_crashlytics_export_strings.xml
64 | crashlytics.properties
65 | crashlytics-build.properties
66 | fabric.properties
67 |
68 | # Editor-based Rest Client
69 | .idea/httpRequests
70 |
71 | # Android studio 3.1+ serialized cache file
72 | .idea/caches/build_file_checksums.ser
73 |
74 | ### Go template
75 | # Binaries for programs and plugins
76 | *.exe
77 | *.exe~
78 | *.dll
79 | *.so
80 | *.dylib
81 |
82 | # Test binary, built with `go test -c`
83 | *.test
84 |
85 | # Output of the go coverage tool, specifically when used with LiteIDE
86 | *.out
87 |
88 | # Dependency directories (remove the comment below to include it)
89 | # vendor/
90 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/copyright/MIT_Crimson_Technologies__LLC.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Crimson Technologies, LLC. All rights reserved.
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 | # adyen
2 | Encrypt secrets for the Adyen payment platform.
3 |
4 | This library uses `crypto/rand` to generate cryptographically secure AES keys and nonces,
5 | and re-uses the same key and nonce for each client. Other publicly available libraries
6 | typically use `math/rand` which is **insecure** for generating secret keys and nonces.
7 |
8 | ## Example
9 |
10 | ```go
11 | package main
12 |
13 | import (
14 | "encoding/hex"
15 | "fmt"
16 | "github.com/CrimsonAIO/adyen"
17 | )
18 |
19 | func main() {
20 | // create a public key from the public key bytes
21 | //
22 | // if you have a key that looks like "10001|...", then you need to
23 | // hex decode the part after "|".
24 | // an example of this is shown here, minus removing the front part.
25 | const plaintextKey = "..."
26 | b, err := hex.DecodeString(plaintextKey)
27 | if err != nil {
28 | panic(err)
29 | }
30 |
31 | // create new encrypter
32 | enc, err := adyen.NewEncrypter("0_1_18", adyen.PubKeyFromBytes(b))
33 | if err != nil {
34 | panic(err)
35 | }
36 |
37 | // encrypt card information
38 | //
39 | // the number and month are automatically formatted with FormatCardNumber and
40 | // FormatMonthYear, so formatting doesn't matter.
41 | payload, err := enc.Encrypt(
42 | "4871049999999910",
43 | "737",
44 | 3,
45 | 2030,
46 | )
47 | if err != nil {
48 | panic(err)
49 | }
50 |
51 | // print the payload to send to the server
52 | fmt.Println(payload)
53 | }
54 | ```
55 | Check it out on [The Go Playground](https://go.dev/play/p/9tL6ziE52aw).
56 |
57 | ## Contributing
58 | Pull requests are welcome to add new version constants or other improvements.
59 | Note that you don't need to use one of our version constants; you can use any string you like.
60 |
61 | If you open a pull request, please use our MIT copyright header.
62 | If you're using GoLand (or any JetBrains IDE) you can do this by going in Settings -> Editor -> Copyright
63 | and selecting the copyright profile found in `.idea/copyright/MIT_Crimson_Technologies_LLC.xml`.
64 | You are welcome to add your own name for your contributions.
--------------------------------------------------------------------------------
/browser_info.go:
--------------------------------------------------------------------------------
1 | package adyen
2 |
3 | // BrowserInfo represents the "browserInfo" object as defined in the Adyen documentation.
4 | //
5 | // Read more: https://docs.adyen.com/online-payments/3d-secure/api-reference#browserinfo
6 | type BrowserInfo struct {
7 | // AcceptHeader is the value of the browser's Accept header.
8 | AcceptHeader string `json:"acceptHeader"`
9 |
10 | // ColorDepth is the browser's color depth in bits per pixel.
11 | // This is the "screen.colorDepth" value.
12 | ColorDepth int `json:"colorDepth"`
13 |
14 | // JavaEnabled is if the browser is capable of executing Java.
15 | JavaEnabled bool `json:"javaEnabled"`
16 |
17 | // JavaScriptEnabled is an optional field indicating if the browser
18 | // is capable of executing JavaScript. If not specified, the Adyen
19 | // API assumes this value is true.
20 | //
21 | // To use a boolean value, use Bool.
22 | JavaScriptEnabled *bool `json:"javaScriptEnabled,omitempty"`
23 |
24 | // Language is the name of the language used by the browser.
25 | // This is the value of "navigator.language".
26 | Language string `json:"language"`
27 |
28 | // ScreenHeight is the browser's screen height.
29 | ScreenHeight int `json:"screenHeight"`
30 |
31 | // ScreenWidth is the browser's screen width.
32 | ScreenWidth int `json:"screenWidth"`
33 |
34 | // TimeZoneOffset is the time difference between UTC time and the browser's
35 | // local time in minutes.
36 | TimeZoneOffset int `json:"timeZoneOffset"`
37 |
38 | // UserAgent is the browser's user agent.
39 | UserAgent string `json:"userAgent"`
40 | }
41 |
42 | // Bool returns a pointer to value.
43 | func Bool(value bool) *bool {
44 | return &value
45 | }
46 |
--------------------------------------------------------------------------------
/card_type.go:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (C) 2022 Crimson Technologies, LLC. All rights reserved.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package adyen
26 |
27 | import "regexp"
28 |
29 | var (
30 | mastercardPattern = regexp.MustCompile(`^(5[1-5]\d{0,14}|2[2-7]\d{0,14})$`)
31 | visadankortPattern = regexp.MustCompile(`^(4571)\d{0,12}$`)
32 | visaPattern = regexp.MustCompile(`^4\d{0,18}$`)
33 | amexPattern = regexp.MustCompile(`^3[47]\d{0,13}$`)
34 | dinersPattern = regexp.MustCompile(`^(36)\d{0,12}$`)
35 | maestroukPattern = regexp.MustCompile(`^(6759)\d{0,15}$`)
36 | soloPattern = regexp.MustCompile(`^(6767)\d{0,15}$`)
37 | laserPattern = regexp.MustCompile(`^(6304|6706|6709|6771)\d{0,15}$`)
38 | discoverPattern = regexp.MustCompile(`^(6011\d{0,12}|(644|645|646|647|648|649)\d{0,13}|65\d{0,14})$`)
39 | jcbPattern = regexp.MustCompile(`^(352[8,9]\d{0,15}|35[4-8]\d{0,16})$`)
40 | bcmcPattern = regexp.MustCompile(`^((6703)\d{0,15}|(479658|606005)\d{0,13})$`)
41 | bijcardPattern = regexp.MustCompile(`^(5100081)\d{0,9}$`)
42 | dankortPattern = regexp.MustCompile(`^(5019)\d{0,12}$`)
43 | hipercardPattern = regexp.MustCompile(`^(606282)\d{0,10}$`)
44 | cupPattern = regexp.MustCompile(`^(62|81)\d{0,17}$`)
45 | maestroPattern = regexp.MustCompile(`^(5[0|6-8]\d{0,17}|6\d{0,18})$`)
46 | eloPattern = regexp.MustCompile(`^((((506699)|(506770)|(506771)|(506772)|(506773)|(506774)|(506775)|(506776)|(506777)|(506778)|(401178)|(438935)|(451416)|(457631)|(457632)|(504175)|(627780)|(636368)|(636297))\d{0,10})|((50676)|(50675)|(50674)|(50673)|(50672)|(50671)|(50670))\d{0,11})$`)
47 | uatpPattern = regexp.MustCompile(`^1\d{0,14}$`)
48 | cartebancairePattern = regexp.MustCompile(`^[4-6]\d{0,15}$`)
49 | visaAlphaBankBonusPattern = regexp.MustCompile(`^(450903)\d{0,10}$`)
50 | mcAlphaBankBonusPattern = regexp.MustCompile(`^(510099)\d{0,10}$`)
51 | hiperPattern = regexp.MustCompile(`^(637095|637568|637599|637609|637612)\d{0,10}$`)
52 | oasisPattern = regexp.MustCompile(`^(982616)\d{0,10}$`)
53 | karenMillenPattern = regexp.MustCompile(`^(98261465)\d{0,8}$`)
54 | warehousePattern = regexp.MustCompile(`^(982633)\d{0,10}$`)
55 | mirPattern = regexp.MustCompile(`^(220)\d{0,16}$`)
56 | codensaPattern = regexp.MustCompile(`^(590712)\d{0,10}$`)
57 | naranjaPattern = regexp.MustCompile(`^(37|40|5[28])([279])\d*$`)
58 | cabalPattern = regexp.MustCompile(`^(58|6[03])([03469])\d*$`)
59 | shoppingPattern = regexp.MustCompile(`^(27|58|60)([39])\d*$`)
60 | argenCardPattern = regexp.MustCompile(`^(50)(1)\d*$`)
61 | troyPattern = regexp.MustCompile(`^(97)(9)\d*$`)
62 | forbrugsforeningenPattern = regexp.MustCompile(`^(60)(0)\d*$`)
63 | vpayPattern = regexp.MustCompile(`^(40[1,8]|413|43[4,5]|44[1,23467]|45[5,8]|46[0,136]|47[1,9]|48[2,37])\d{0,16}$`)
64 | rupayPattern = regexp.MustCompile(`^(100003|508(2|[5-9])|60(69|[7-8])|652(1[5-9]|[2-5]\d|8[5-9])|65300[3-4]|8172([0-1]|[3-5]|7|9)|817(3[3-8]|40[6-9]|410)|35380([0-2]|[5-6]|9))\d{0,12}$`)
65 | )
66 |
67 | // DetectCardType detects the type of the given card number.
68 | // The card number must have no whitespace characters.
69 | //
70 | // If the card's type cannot be detected, then "noBrand" is returned
71 | // which is also what Adyen uses if it cannot detect the card type.
72 | func DetectCardType(formattedCardNumber string) string {
73 | switch {
74 | case mastercardPattern.MatchString(formattedCardNumber):
75 | return "mc"
76 | case visadankortPattern.MatchString(formattedCardNumber):
77 | return "visadankort"
78 | case visaPattern.MatchString(formattedCardNumber):
79 | return "visa"
80 | case amexPattern.MatchString(formattedCardNumber):
81 | return "amex"
82 | case dinersPattern.MatchString(formattedCardNumber):
83 | return "diners"
84 | case maestroukPattern.MatchString(formattedCardNumber):
85 | return "maestrouk"
86 | case soloPattern.MatchString(formattedCardNumber):
87 | return "solo"
88 | case laserPattern.MatchString(formattedCardNumber):
89 | return "laser"
90 | case discoverPattern.MatchString(formattedCardNumber):
91 | return "discover"
92 | case jcbPattern.MatchString(formattedCardNumber):
93 | return "jcb"
94 | case bcmcPattern.MatchString(formattedCardNumber):
95 | return "bcmc"
96 | case bijcardPattern.MatchString(formattedCardNumber):
97 | return "bijcard"
98 | case dankortPattern.MatchString(formattedCardNumber):
99 | return "dankort"
100 | case hipercardPattern.MatchString(formattedCardNumber):
101 | return "hiper"
102 | case cupPattern.MatchString(formattedCardNumber):
103 | return "cup"
104 | case maestroPattern.MatchString(formattedCardNumber):
105 | return "maestro"
106 | case eloPattern.MatchString(formattedCardNumber):
107 | return "elo"
108 | case uatpPattern.MatchString(formattedCardNumber):
109 | return "uatp"
110 | case cartebancairePattern.MatchString(formattedCardNumber):
111 | return "cartebancaire"
112 | case visaAlphaBankBonusPattern.MatchString(formattedCardNumber):
113 | return "visaalphabankbonus"
114 | case mcAlphaBankBonusPattern.MatchString(formattedCardNumber):
115 | return "mcalphabankbonus"
116 | case hiperPattern.MatchString(formattedCardNumber):
117 | return "hiper"
118 | case oasisPattern.MatchString(formattedCardNumber):
119 | return "oasis"
120 | case karenMillenPattern.MatchString(formattedCardNumber):
121 | return "karenmillen"
122 | case warehousePattern.MatchString(formattedCardNumber):
123 | return "warehouse"
124 | case mirPattern.MatchString(formattedCardNumber):
125 | return "mir"
126 | case codensaPattern.MatchString(formattedCardNumber):
127 | return "codensa"
128 | case naranjaPattern.MatchString(formattedCardNumber):
129 | return "naranja"
130 | case cabalPattern.MatchString(formattedCardNumber):
131 | return "cabal"
132 | case shoppingPattern.MatchString(formattedCardNumber):
133 | return "shopping"
134 | case argenCardPattern.MatchString(formattedCardNumber):
135 | return "argencard"
136 | case troyPattern.MatchString(formattedCardNumber):
137 | return "troy"
138 | case forbrugsforeningenPattern.MatchString(formattedCardNumber):
139 | return "forbrugsforeningen"
140 | case vpayPattern.MatchString(formattedCardNumber):
141 | return "vpay"
142 | case rupayPattern.MatchString(formattedCardNumber):
143 | return "rupay"
144 | default:
145 | return "noBrand"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/card_type_test.go:
--------------------------------------------------------------------------------
1 | package adyen
2 |
3 | import "testing"
4 |
5 | func TestDetectCardType(t *testing.T) {
6 | if DetectCardType("5123459046058920") != "mc" {
7 | t.Fail()
8 | }
9 |
10 | if DetectCardType("4000180000000002") != "visa" {
11 | t.Fail()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/encrypt.go:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (C) 2022 Crimson Technologies, LLC. All rights reserved.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package adyen
26 |
27 | import (
28 | "crypto/rand"
29 | "crypto/rsa"
30 | "encoding/base64"
31 | "encoding/json"
32 | "fmt"
33 | "github.com/CrimsonAIO/aesccm"
34 | )
35 |
36 | const (
37 | // GenerationTimeKey is the JSON key for the generated time.
38 | GenerationTimeKey = "generationtime"
39 |
40 | // GenerationTimeFormat is the time format.
41 | // This is identical to time.RFC3339Nano except there is only three trailing zeros.
42 | GenerationTimeFormat = "2006-01-02T15:04:05.000Z07:00"
43 |
44 | // KeyNumber is the card number field key.
45 | KeyNumber = "number"
46 |
47 | // KeyExpiryMonth is the expiry month field key.
48 | KeyExpiryMonth = "expiryMonth"
49 |
50 | // KeyExpiryYear is the expiry year field key.
51 | KeyExpiryYear = "expiryYear"
52 |
53 | // KeySecurityCode is the security code field key.
54 | KeySecurityCode = "cvc"
55 | )
56 |
57 | // Encrypt encrypts a card number, security code (CVV/CVC), expiry month and year
58 | // into a map and correctly formats all values using FormatCardNumber and FormatMonthYear.
59 | func (enc *Encrypter) Encrypt(number, securityCode string, month, year int) (string, error) {
60 | m, y := FormatMonthYear(month, year)
61 | return enc.EncryptFields(map[string]string{
62 | KeyNumber: FormatCardNumber(number),
63 | KeyExpiryMonth: m,
64 | KeyExpiryYear: y,
65 | KeySecurityCode: securityCode,
66 | })
67 | }
68 |
69 | // EncryptField encrypts a single key and value.
70 | func (enc *Encrypter) EncryptField(key, value string) (string, error) {
71 | return enc.EncryptFields(map[string]string{key: value})
72 | }
73 |
74 | // EncryptFields encrypts a map.
75 | func (enc *Encrypter) EncryptFields(fields map[string]string) (string, error) {
76 | if _, ok := fields[GenerationTimeKey]; !ok {
77 | fields[GenerationTimeKey] = enc.GetGenerationTime().Format(GenerationTimeFormat)
78 | }
79 |
80 | encoded, err := json.Marshal(fields)
81 | if err != nil {
82 | return "", err
83 | }
84 | return enc.EncryptPlaintext(encoded)
85 | }
86 |
87 | // EncryptPlaintext seals the given plaintext and returns the sealed content in the Adyen format.
88 | //
89 | // Most callers should use Encrypt, EncryptField or EncryptFields instead.
90 | func (enc *Encrypter) EncryptPlaintext(plaintext []byte) (string, error) {
91 | // generate random nonce
92 | nonce := make([]byte, 12)
93 | if _, err := rand.Read(nonce); err != nil {
94 | return "", err
95 | }
96 |
97 | // create ccm cipher
98 | ccm, err := aesccm.NewCCM(enc.block, len(nonce), 8)
99 | if err != nil {
100 | return "", err
101 | }
102 |
103 | // create ciphertext
104 | ciphertext := ccm.Seal(nil, nonce, plaintext, nil)
105 | // nonce with ciphertext
106 | nonceWithCiphertext := append(nonce, ciphertext...)
107 |
108 | // encrypted key using public key
109 | sealedKey, err := rsa.EncryptPKCS1v15(rand.Reader, enc.pubKey, enc.key[:])
110 | if err != nil {
111 | return "", err
112 | }
113 |
114 | return fmt.Sprintf(
115 | "adyenjs_%s$%s$%s",
116 | enc.Version,
117 | base64.StdEncoding.EncodeToString(sealedKey),
118 | base64.StdEncoding.EncodeToString(nonceWithCiphertext),
119 | ), nil
120 | }
121 |
--------------------------------------------------------------------------------
/encrypter.go:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (C) 2022 Crimson Technologies, LLC. All rights reserved.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package adyen
26 |
27 | import (
28 | "crypto/aes"
29 | "crypto/cipher"
30 | "crypto/rand"
31 | "crypto/rsa"
32 | "math/big"
33 | "time"
34 | )
35 |
36 | // GenerationTimeFunc is a function responsible for returning the time that
37 | // a payload was generated at.
38 | type GenerationTimeFunc func() time.Time
39 |
40 | // An Encrypter encrypts content into the Adyen format
41 | // using an RSA public key and AES-256.
42 | type Encrypter struct {
43 | // pubKey is the RSA public key to use to seal key.
44 | pubKey *rsa.PublicKey
45 |
46 | // key and block are used for AES encryption.
47 | // Both are set by Reset and should not be written to
48 | // from anywhere else.
49 | key [32]byte
50 | block cipher.Block
51 |
52 | // Version is the Adyen version that this Encrypter will
53 | // seal plaintext for.
54 | Version string
55 |
56 | // GetGenerationTime gets the time.Time to use for the
57 | // required "generationtime" JSON field. The default is
58 | // time.Now.
59 | //
60 | // This may be modified by the caller to return custom times
61 | // that differ from the default.
62 | GetGenerationTime GenerationTimeFunc
63 | }
64 |
65 | // Reset resets the AES key and cipher block for the encrypter to use.
66 | //
67 | // If err != nil, the Encrypter is not safe to use.
68 | func (enc *Encrypter) Reset() (err error) {
69 | if _, err = rand.Read(enc.key[:]); err != nil {
70 | return
71 | }
72 |
73 | enc.block, err = aes.NewCipher(enc.key[:])
74 | return
75 | }
76 |
77 | // NewEncrypter creates a new Encrypter with the given version and RSA public key.
78 | //
79 | // Calls to Encrypter.EncryptPlaintext will panic if pubKey == nil.
80 | func NewEncrypter(version string, pubKey *rsa.PublicKey) (enc *Encrypter, err error) {
81 | enc = &Encrypter{pubKey: pubKey}
82 | enc.Version = version
83 | enc.GetGenerationTime = time.Now
84 | err = enc.Reset()
85 | return
86 | }
87 |
88 | // PubKeyFromBytes creates a new RSA public key from b with the optional public exponent.
89 | func PubKeyFromBytes(b []byte, publicExponent ...int) *rsa.PublicKey {
90 | key := new(rsa.PublicKey)
91 | key.N = new(big.Int).SetBytes(b)
92 |
93 | if len(publicExponent) == 0 {
94 | key.E = 65537
95 | } else {
96 | key.E = publicExponent[0]
97 | }
98 |
99 | return key
100 | }
101 |
--------------------------------------------------------------------------------
/encrypter_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (C) 2022 Crimson Technologies, LLC. All rights reserved.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package adyen
26 |
27 | import (
28 | "crypto/rand"
29 | "crypto/rsa"
30 | "testing"
31 | )
32 |
33 | func TestEncrypter(t *testing.T) {
34 | // generate random key
35 | key, err := rsa.GenerateKey(rand.Reader, 1024)
36 | if err != nil {
37 | panic(err)
38 | }
39 |
40 | enc, err := NewEncrypter("v1", &key.PublicKey)
41 | if err != nil {
42 | panic(err)
43 | }
44 |
45 | payload, err := enc.Encrypt(
46 | "4871 0499 9999 9910",
47 | "737",
48 | 3,
49 | 2030,
50 | )
51 | if err != nil {
52 | panic(err)
53 | }
54 |
55 | t.Log("Payload:", payload)
56 | }
57 |
--------------------------------------------------------------------------------
/format.go:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (C) 2022 Crimson Technologies, LLC. All rights reserved.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package adyen
26 |
27 | import (
28 | "fmt"
29 | "strconv"
30 | "strings"
31 | "time"
32 | )
33 |
34 | // FormatCardNumber formats the given card number into the Adyen format.
35 | // Numbers less than 15 digits (excluding white space) are ignored.
36 | //
37 | // Examples:
38 | //
39 | // 0123456789012345 -> 0123 4567 8901 2345
40 | //
41 | // 0123 4567 8901 2345 -> (no change)
42 | //
43 | // 0123 456789012345 -> 0123 4567 8901 2345
44 | func FormatCardNumber(number string) string {
45 | if cnt := strings.Count(number, " "); cnt == 4 {
46 | // we assume the number is already formatted if there are exactly 4 spaces.
47 | return number
48 | } else if cnt > 0 {
49 | // else if there was at least 1 space, replace them all.
50 | number = strings.ReplaceAll(number, " ", "")
51 | }
52 | // ignore if the number is less than 15 digits.
53 | if len(number) < 15 {
54 | return number
55 | }
56 |
57 | return number[:4] + " " + number[4:8] + " " + number[8:12] + " " + number[12:]
58 | }
59 |
60 | // FormatMonthYear formats a card expiry month and year into the Adyen format.
61 | // It is assumed that the given year is the fully-qualified year,
62 | // like "2020" (instead of "20".)
63 | //
64 | // Examples:
65 | //
66 | // 5, 2024 -> "05", "2024"
67 | //
68 | // 12, 2024 -> "12", "2024"
69 | func FormatMonthYear[T time.Month | int](month T, year int) (string, string) {
70 | return fmt.Sprintf("%02d", month), strconv.Itoa(year)
71 | }
72 |
--------------------------------------------------------------------------------
/format_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (C) 2022 Crimson Technologies, LLC. All rights reserved.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package adyen
26 |
27 | import "testing"
28 |
29 | func TestFormatCardNumber(t *testing.T) {
30 | test := func(number, expected string) {
31 | if formatted := FormatCardNumber(number); formatted != expected {
32 | t.Fatalf("%s should be %s, instead got %s\n", number, expected, formatted)
33 | }
34 | }
35 |
36 | test("0123456789012345", "0123 4567 8901 2345")
37 | test("0123 4567 8901 2345", "0123 4567 8901 2345")
38 | test("0123 456789012345", "0123 4567 8901 2345")
39 | }
40 |
41 | func TestFormatMonthYear(t *testing.T) {
42 | test := func(m, y int, em, ey string) {
43 | if fm, fy := FormatMonthYear(m, y); fm != em || fy != ey {
44 | t.Fatalf("(%d, %d) should be (%s, %s), instead got (%s, %s)\n", m, y, em, ey, fm, fy)
45 | }
46 | }
47 |
48 | test(5, 2024, "05", "2024")
49 | test(12, 2024, "12", "2024")
50 | }
51 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/CrimsonAIO/adyen
2 |
3 | go 1.18
4 |
5 | require github.com/CrimsonAIO/aesccm v1.0.1
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/CrimsonAIO/aesccm v1.0.1 h1:bLrdwVaCEYNk/TPa/P3zo22oIVlYrHq47RTuc2RavGs=
2 | github.com/CrimsonAIO/aesccm v1.0.1/go.mod h1:/5yapD0l7aiLjO/jw0IQiTVlIOY7Lalo8AH8zXqhDxM=
3 |
--------------------------------------------------------------------------------