I’m using CyberSource as a payment processor and I want to convert the JS hashing code to Golang. I attempted to do it but the output of the Go is not matching that of the JS. Where am I messing up? The Golang code runs and spits out a long string about the same length as the JS, but not the same.
Correct JS
"use strict";
import crypto from 'crypto';
export const CardTypes = {
Visa: '001',
MasterCard: '002',
AmericanExpress: '003',
Discover: '004',
Diners: '005',
JCB: '007',
Maestro: '042',
ChinaUnionPay: '062',
};
export const generateKey = (crypto) => crypto.subtle.generateKey({
name: 'AES-GCM',
length: 256
}, true, ['encrypt']);
export const _encrypt = async (crypto, payload, key, header, iv) => {
const algorithm = {
name: 'AES-GCM',
iv,
additionalData: stringToArrayBuffer(replace(JSON.stringify(header))),
tagLength: 128
};
const buffer = await crypto.subtle.encrypt(algorithm, key, stringToArrayBuffer(JSON.stringify(payload)));
return [buffer, key];
};
export const importKey = (crypto, jsonWebKey) => crypto.subtle.importKey('jwk', jsonWebKey, {
name: 'RSA-OAEP',
hash: {
name: 'SHA-1'
}
}, false, ['wrapKey']);
export const wrapKey = async (crypto, key, jsonWebKey) => {
const wrappingKey = await importKey(crypto, jsonWebKey);
const params = {
name: 'RSA-OAEP',
hash: {
name: 'SHA-1'
}
};
return crypto.subtle.wrapKey('raw', key, wrappingKey, params);
};
export const buildEncryptedData = async (crypto, buffer, key, iv, header, jsonWebKey) => {
const u = buffer.byteLength - ((128 + 7) >> 3);
const keyBuffer = await wrapKey(crypto, key, jsonWebKey);
return [
replace(JSON.stringify(header)),
replace(arrayBufferToString(keyBuffer)),
replace(arrayBufferToString(iv)),
replace(arrayBufferToString(buffer.slice(0, u))),
replace(arrayBufferToString(buffer.slice(u)))
].join('.');
};
export const arrayBufferToString = (buf) => String.fromCharCode.apply(null, new Uint8Array(buf));
export const stringToArrayBuffer = (str) => {
const buffer = new ArrayBuffer(str.length);
const array = new Uint8Array(buffer);
const { length } = str;
for (let r = 0; r < length; r += 1) {
array[r] = str.charCodeAt(r);
}
return buffer;
};
// Replaced base64 encoding function
export const replace = (str) => btoa(str).replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');
// JWT decoding function
function decodeJwt(token) {
try {
const payload = token.split('.')[1]; // Extract the payload part of the JWT
const decoded = Buffer.from(payload, 'base64').toString('utf-8'); // Decode base64-encoded string
return JSON.parse(decoded); // Parse the JSON string
} catch (error) {
console.log(error);
throw new Error('Invalid JWT token');
}
}
// Encryption function
export const encrypt = async (data, context, index = 0) => {
const keyId = decodeJwt(context);
const header = {
kid: keyId.flx.jwk.kid,
alg: 'RSA-OAEP',
enc: 'A256GCM'
};
const payload = {
data,
context,
index
};
const iv = crypto.randomBytes(12); // Generate a random initialization vector (12 bytes for AES-GCM)
return generateKey(crypto)
.then((key) => _encrypt(crypto, payload, key, header, iv))
.then((data) => {
const [buffer, key] = data;
return buildEncryptedData(crypto, buffer, key, iv, header, keyId.flx.jwk);
});
};
// Example use
const context = "eyJraWQiOiJ3ZiIsImFsZyI6IlJTMjU2In0.eyJmbHgiOnsicGF0aCI6Ii9mbGV4L3YyL3Rva2VucyIsImRhdGEiOiIvUVJ1QjF1dGJVQXd3OXp0ekovZG1oQUFFTlB3VFVZR2dtVTh1eDBiZnNocWpFNmlEU09VNWxJd0dFZGNNWG1nSlp5WGlrWDA0RXVDNmlrOXJPaXNZZW5XRm1ZK0ZUSGw4dW9UYWkvOVhRNEZ6SDFPOE4rekxkaEkzblN3QllwQmsyWWYiLCJvcmlnaW4iOiJodHRwczovL2ZsZXguY3liZXJzb3VyY2UuY29tIiwiandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwibiI6InNfbHhBLUJXc1Bab1pRR0lKN0JwdjNfNkF5U0VYZXVtMloyR3RzM1NFY2preFM0aXFDTmltN1haTzZDanBMMy1laW1aZVZmNmo2YjFkbG5NUGViZDRCRHduQlotU3ZVQ1VhRjBhWGNxazhheGFBVVhNZlg0VEthR1BOcldmc2x4ZFhEdHk5empsY3dLbTBKUkhEdGpSVDBKQkVqZWQ3aXZDUGVnWV9ySldkSGtDVEpPN2t4dDl0YWFfQzZsclYwMGRHLWlmVGZRTE1rZmw0cHFKZzN0QzJiSGNrTFZIX2hET0loR3BkZGRQNkpiNUdLUW9GblcwX0VfcFk3WWF0WVFTdkZLOVZYUE82azJDbUZuaUNpYS1ERFhDMkFGdnlqdFA0bWdDOUdHbEVkWjczRDFyVExnZG9TTEVOOFBFS3VuZUFiaG5oWjB0QkM2RUpPcFIyRlB2USIsImtpZCI6IjAzd3pWcDgzRzFMV0l5ZXo4NTFNNzh1YWRzZmFnd1RmIn19LCJjdHgiOlt7ImRhdGEiOnsiY2xpZW50TGlicmFyeSI6Imh0dHBzOi8vZmxleC5jeWJlcnNvdXJjZS5jb20vbWljcm9mb3JtL2J1bmRsZS92MS9mbGV4LW1pY3JvZm9ybS5taW4uanMiLCJ0YXJnZXRPcmlnaW5zIjpbImh0dHBzOi8vd3d3LnBva2Vtb25jZW50ZXIuY29tIiwiaHR0cHM6Ly90ZXN0LnBva2Vtb25jZW50ZXIuY29tIiwiaHR0cDovL2xvY2FsaG9zdDozMDAwIl0sIm1mT3JpZ2luIjoiaHR0cHM6Ly9mbGV4LmN5YmVyc291cmNlLmNvbSJ9LCJ0eXBlIjoibWYtMS4wLjAifV0sImlzcyI6IkZsZXggQVBJIiwiZXhwIjoxNzMxNzQzMjQ5LCJpYXQiOjE3MzE3NDIzNDksImp0aSI6IkloRmhQS0ZJYklodFJlSnUifQ.NXO01kzfvAVqs-iZY2r7dKL_0enRa3snbAe0kpwCs-rrLDbwITAYw--sfX1rC0CLBxeFubaT52y0hxCf88-MmcQetSchswCka9MBl7zLnmncsToES1rd7VaDbv_DlgaHBwREb1NR3l-6rmclcgOswK_kuAgFRWes-QOLWgtSqoBfhOC87w1ZInNMDwcJk__74o8vBgsVB7UDHfyjDaRMRrxOZRiX3TTmizOZtuN0TvmL7rwJHgTxlAdwcuxA4Ja2hBLA0zvqLgDg5cPZCNIpQvGYTdzJFSSc-nmhYlzf4eqZ3V-eD3qK9u1q44ZWAl8dS9x56XrsIkvYMo9VJ3namQ";
const data = {
number: "5102778094092647",
securityCode: "813",
expirationMonth: "01",
expirationYear: "2025",
type: "002",
};
const encrypted = await encrypt(data, context);
Golang
package Captcha
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"strings"
)
var CardTypes = map[string]string{
"Visa": "001",
"MasterCard": "002",
"AmericanExpress": "003",
"Discover": "004",
"Diners": "005",
"JCB": "007",
"Maestro": "042",
"ChinaUnionPay": "062",
}
func decodeJwt(token string) (map[string]interface{}, error) {
parts := strings.Split(token, ".")
if len(parts) < 2 {
return nil, errors.New("invalid JWT token: missing segments")
}
payload := parts[1]
decoded, err := base64.RawURLEncoding.DecodeString(padBase64(payload))
if err != nil {
decodedStd, errStd := base64.StdEncoding.DecodeString(padBase64(payload))
if errStd != nil {
return nil, fmt.Errorf("failed to decode base64 payload: %w", err)
}
decoded = decodedStd
}
var result map[string]interface{}
if err := json.Unmarshal(decoded, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal payload JSON: %w", err)
}
return result, nil
}
func padBase64(s string) string {
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
return s
}
func generateKey() ([]byte, error) {
key := make([]byte, 32) // 256 bits
_, err := rand.Read(key)
if err != nil {
return nil, err
}
return key, nil
}
func stringToBytes(str string) []byte {
return []byte(str)
}
func bytesToString(buf []byte) string {
return string(buf)
}
func replace(str string) string {
return base64.RawURLEncoding.EncodeToString([]byte(str))
}
func _encrypt(payload interface{}, key []byte, header interface{}, iv []byte) ([]byte, []byte, error) {
// Prepare the additionalData from the header
headerJSON, err := json.Marshal(header)
if err != nil {
return nil, nil, err
}
additionalData := stringToBytes(replace(string(headerJSON)))
// Convert payload to JSON
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
// Encrypt
// GCM output = ciphertext + tag appended
cipherText := aesGCM.Seal(nil, iv, payloadBytes, additionalData)
// Return the combined ciphertext+tag, along with key
return cipherText, key, nil
}
type jwkRSA struct {
Kty string `json:"kty"`
N string `json:"n"` // base64url
E string `json:"e"` // base64url
Kid string `json:"kid"` // optional
Use string `json:"use"` // e.g. "enc"
Alg string `json:"alg"` // e.g. "RSA-OAEP"
}
func wrapKey(aesKey []byte, jwk map[string]interface{}) ([]byte, error) {
// Convert the relevant JWK portion to a jwkRSA struct
var keyData jwkRSA
b, err := json.Marshal(jwk)
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &keyData); err != nil {
return nil, err
}
// Decode base64 URL n and e
nBytes, err := base64.RawURLEncoding.DecodeString(keyData.N)
if err != nil {
return nil, fmt.Errorf("failed to decode n: %w", err)
}
eBytes, err := base64.RawURLEncoding.DecodeString(keyData.E)
if err != nil {
return nil, fmt.Errorf("failed to decode e: %w", err)
}
// Convert big-endian bytes to big.Int
n := new(big.Int).SetBytes(nBytes)
e := new(big.Int).SetBytes(eBytes)
// If e is small, interpret it properly
// (Often e is 65537)
pubKey := &rsa.PublicKey{
N: n,
E: int(e.Int64()),
}
// For RSA-OAEP with SHA1
hash := sha1.New()
// Encrypt (wrap) the AES key
wrapped, err := rsa.EncryptOAEP(hash, rand.Reader, pubKey, aesKey, nil)
if err != nil {
return nil, err
}
return wrapped, nil
}
func buildEncryptedData(
cipherText []byte,
aesKey []byte,
iv []byte,
header interface{},
jwk map[string]interface{},
) (string, error) {
// GCM uses a 128-bit (16-byte) tag
tagLength := 16
// ciphertext length minus 16 bytes
if len(cipherText) < tagLength {
return "", errors.New("ciphertext too short for GCM tag")
}
u := len(cipherText) - tagLength
// Wrap the AES key
wrappedKey, err := wrapKey(aesKey, jwk)
if err != nil {
return "", err
}
// Convert everything to base64-URL strings
headerJSON, _ := json.Marshal(header)
partHeader := replace(string(headerJSON))
partKey := replace(bytesToString(wrappedKey))
partIV := replace(bytesToString(iv))
partCipher := replace(bytesToString(cipherText[:u]))
partTag := replace(bytesToString(cipherText[u:]))
return strings.Join([]string{
partHeader,
partKey,
partIV,
partCipher,
partTag,
}, "."), nil
}
func Encrypt(data interface{}, context string, index int) (string, string, error) {
// 1) Decode the JWT to get the kid + JWK
decoded, err := decodeJwt(context)
if err != nil {
return "", "", err
}
flxRaw, ok := decoded["flx"]
if !ok {
return "", "", errors.New("flx field not found in JWT payload")
}
flxMap, ok := flxRaw.(map[string]interface{})
if !ok {
return "", "", errors.New("invalid flx field")
}
jwkRaw, ok := flxMap["jwk"]
if !ok {
return "", "", errors.New("jwk field not found in flx")
}
jwkMap, ok := jwkRaw.(map[string]interface{})
if !ok {
return "", "", errors.New("invalid jwk field")
}
kidRaw, ok := jwkMap["kid"]
if !ok {
return "", "", errors.New("kid field not found in jwk")
}
kid, _ := kidRaw.(string)
fmt.Println("key id is: " + kid)
// 2) Build the header
header := map[string]string{
"kid": kid,
"alg": "RSA-OAEP",
"enc": "A256GCM",
}
// 3) The actual payload
payload := map[string]interface{}{
"data": data,
"context": context,
"index": index,
}
// 4) Generate a random IV (12 bytes for GCM)
iv := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", "", err
}
// 5) Generate the AES key
aesKey, err := generateKey()
if err != nil {
return "", "", err
}
// 6) Encrypt with AES-GCM
cipherText, realKey, err := _encrypt(payload, aesKey, header, iv)
if err != nil {
return "", "", err
}
// 7) Build final encrypted data string
encrypted, err := buildEncryptedData(cipherText, realKey, iv, header, jwkMap)
if err != nil {
return "", "", err
}
return encrypted, kid, nil
}
// Example use
func main() {
context := "eyJraWQiOiJ3ZiIsImFsZyI6IlJTMjU2In0.eyJmbHgiOnsicGF0aCI6Ii9mbGV4L3YyL3Rva2VucyIsImRhdGEiOiIvUVJ1QjF1dGJVQXd3OXp0ekovZG1oQUFFTlB3VFVZR2dtVTh1eDBiZnNocWpFNmlEU09VNWxJd0dFZGNNWG1nSlp5WGlrWDA0RXVDNmlrOXJPaXNZZW5XRm1ZK0ZUSGw4dW9UYWkvOVhRNEZ6SDFPOE4rekxkaEkzblN3QllwQmsyWWYiLCJvcmlnaW4iOiJodHRwczovL2ZsZXguY3liZXJzb3VyY2UuY29tIiwiandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwibiI6InNfbHhBLUJXc1Bab1pRR0lKN0JwdjNfNkF5U0VYZXVtMloyR3RzM1NFY2preFM0aXFDTmltN1haTzZDanBMMy1laW1aZVZmNmo2YjFkbG5NUGViZDRCRHduQlotU3ZVQ1VhRjBhWGNxazhheGFBVVhNZlg0VEthR1BOcldmc2x4ZFhEdHk5empsY3dLbTBKUkhEdGpSVDBKQkVqZWQ3aXZDUGVnWV9ySldkSGtDVEpPN2t4dDl0YWFfQzZsclYwMGRHLWlmVGZRTE1rZmw0cHFKZzN0QzJiSGNrTFZIX2hET0loR3BkZGRQNkpiNUdLUW9GblcwX0VfcFk3WWF0WVFTdkZLOVZYUE82azJDbUZuaUNpYS1ERFhDMkFGdnlqdFA0bWdDOUdHbEVkWjczRDFyVExnZG9TTEVOOFBFS3VuZUFiaG5oWjB0QkM2RUpPcFIyRlB2USIsImtpZCI6IjAzd3pWcDgzRzFMV0l5ZXo4NTFNNzh1YWRzZmFnd1RmIn19LCJjdHgiOlt7ImRhdGEiOnsiY2xpZW50TGlicmFyeSI6Imh0dHBzOi8vZmxleC5jeWJlcnNvdXJjZS5jb20vbWljcm9mb3JtL2J1bmRsZS92MS9mbGV4LW1pY3JvZm9ybS5taW4uanMiLCJ0YXJnZXRPcmlnaW5zIjpbImh0dHBzOi8vd3d3LnBva2Vtb25jZW50ZXIuY29tIiwiaHR0cHM6Ly90ZXN0LnBva2Vtb25jZW50ZXIuY29tIiwiaHR0cDovL2xvY2FsaG9zdDozMDAwIl0sIm1mT3JpZ2luIjoiaHR0cHM6Ly9mbGV4LmN5YmVyc291cmNlLmNvbSJ9LCJ0eXBlIjoibWYtMS4wLjAifV0sImlzcyI6IkZsZXggQVBJIiwiZXhwIjoxNzMxNzQzMjQ5LCJpYXQiOjE3MzE3NDIzNDksImp0aSI6IkloRmhQS0ZJYklodFJlSnUifQ.NXO01kzfvAVqs-iZY2r7dKL_0enRa3snbAe0kpwCs-rrLDbwITAYw--sfX1rC0CLBxeFubaT52y0hxCf88-MmcQetSchswCka9MBl7zLnmncsToES1rd7VaDbv_DlgaHBwREb1NR3l-6rmclcgOswK_kuAgFRWes-QOLWgtSqoBfhOC87w1ZInNMDwcJk__74o8vBgsVB7UDHfyjDaRMRrxOZRiX3TTmizOZtuN0TvmL7rwJHgTxlAdwcuxA4Ja2hBLA0zvqLgDg5cPZCNIpQvGYTdzJFSSc-nmhYlzf4eqZ3V-eD3qK9u1q44ZWAl8dS9x56XrsIkvYMo9VJ3namQ"
data := map[string]string{
"number": "5102778094092647",
"securityCode": "813",
"expirationMonth": "01",
"expirationYear": "2025",
"type": "002",
}
encryptedData, kid, err := Captcha.Encrypt(data, context, 0)
}