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