NodeJS Apple Business Chat REST API, Downloading and Decrypting Large interactive message - node.js

I am reading the apple business chat api docs and I'm at the section "Receiving Large Interactive Data Payloads". The last step is to decipher an attachment then send to the Business Chat Api.
The Download & data step Documentation
--- And the decipher instructions DOCUMENTATION --
Then, using a cypher library, decrypt the file by using the AES/CTR/NoPadding algorithm with an all-zero, 16-byte initialization vector (IV) with the key value in the Attachment dictionary for the downloaded attachment.
So here is my interpretation of this documentation as they leave me little to work with.
// The single-use, 256-bit AES key represented as a hex-encoded string.
const algorithm = 'aes-256-ctr';
// remove the 00 prefix from the hex-encoded string,
// then decode the string into its original value.
const key = Buffer.from(decryptKey.substr(2), 'hex');
// Use the decoded key value to decrypt the downloaded attachment file.
// THE FULL IMPLEMENTATION
const iv = Buffer.alloc(16, 0);
const key = Buffer.from(decryptKey.substr(2), 'hex');
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAutoPadding(false)
let decrypted = decipher.update(data, '', 'hex');
decrypted += decipher.final('hex');
console.log("decrypted:", decrypted);
// Finally send to Apple Business Chat Api
POST https://mspgw.push.apple.com/v1/decodePayload
accept: */*
accept-encoding: gzip, deflate
authorization: Bearer signed-web-token
source-id: business-id
bid: some-bid
{ attachment data }
// Here is a piece of the incoming data
��F�ڼ���/��G����+���)�\M���x�tk��Y(���-�-G�ȍ$t��� )
// After decipher
d3ffade249263d1252ee0dcfa6accd0beff31c607889ff0d31d893adde5063616a15591e181fb698350fb955f
Im not sure if I am doing deciphering correctly as when I send the decrypted code to Apples API
POST https://mspgw.push.apple.com/v1/decodePayload
it is always code response 400
I have contacted Apple for assistance on this issue. I will update this doc as soon as I get a response back from them.
Below is a diagram of the steps needed to take. I stuck at the last 2 steps.

Here is the update for solving the deciphering issues using the apple business chat api with NodeJS. Main issue was converting deciphered data to a buffer before sending to Apple to be decoded.
const decryptKeyFromInteractiveRef = "03f30ff3d3d03dc3".toUpperCase()
async function main(decryptKeyFromInteractiveRef) {
const url = await preDownloadUrl();
const data = await downloadPayload(url);
const decipheredData = await decipherInteractiveRef(data);
const decodedData = await appleDecode(decipheredData);
console.log("Finally your data", decodedData);
async function appleDecode(decipheredData) {
var config = {
method: 'post',
url: 'https://mspgw.push.apple.com/v1/decodePayload',
headers: {
"Authorization": Authorization,
"bid": "com.apple.messages.MSMessageExtensionBalloonPlugin:0000000000:com.apple.icloud.apps.messages.business.extension",
"source-id": BIZ_ID,
"accept": "*/*",
"accept-encoding": "gzip, deflate",
'Content-Type': 'application/octet-stream'
},
data: decipheredData
};
const { data } = await axios(config);
const path = Path.resolve(__dirname, 'images', 'data.json')
fs.writeFileSync(path, JSON.stringify(data))
}
async function decipherInteractiveRef() {
const iv = Buffer.alloc(16); // buffer alloc fills with zeros
const key = Buffer.from(decryptKey.slice(2), 'hex',);
const decipher = crypto.createDecipheriv("aes-256-ctr", key, iv);
decipher.setAutoPadding(false); // No Padding
let decrypted = decipher.update(data); // if input is a buffer dont choose a encoding
return decrypted;
}
async function preDownloadUrl() {
//Using the fields in the received interactiveDataRef key,
// retrieve the URL to the payload by calling the /preDownload endpoint.
//interactiveDataRef key
const signatureHex = "81101cc048b6b588c895f01c12715421f9d0a25329".toUpperCase()
const signature = Buffer.from(signatureHex, 'hex').toString('base64')
var configInteractiveRef = {
method: 'get',
url: 'https://mspgw.push.apple.com/v1/preDownload',
headers: {
'Authorization': Authorization,
'source-id': BIZ_ID,
'MMCS-Url': 'https://p56-content.icloud.com/MZ02db38070edccb2ce8c972efdcdd25437439745cad6f15473bb7880d436377702752e134be8bd3b4d695567a5d574142.C01USN00',
'MMCS-Signature': signature,
'MMCS-Owner': 'MZ02db38070edccb2ce8c972efdcdd25437439745cad6f15473bb7880d436377702752e134be8bd3b4d695567a5d574142.C01USN00'
}
};
const response = await axios(configInteractiveRef)
return response.data["download-url"];
}
// download big payload from apple
async function downloadPayload(url) {
const { data } = await axios.get(url, { responseType: 'arraybuffer' });
return data
}}

Related

Authenticating FTX API SHA256 HMAC with Node

I am lost with using HMAC SHA256 for api authentication. This is my first time using it and I'm not sure what I am missing although I suspect it has to do with the timestamp. Can someone please help me identify what it is I am missing?
Everytime I try and make an API call I get a response stating
data: { success: false, error: 'Not logged in: Invalid signature' }
Here are the requirements for making the API call including the HMAC SHA256.
Here is the code I am using currently:
const axios = require('axios');
var forge = require('node-forge');
require('dotenv').config()
// get timestamp
var time = new Date().getTime();
// generate and return hash
function generateHash(plainText,secretKey)
{
var hmac = forge.hmac.create();
hmac.start('sha256', secretKey);
hmac.update(plainText);
var hashText = hmac.digest().toHex();
return hashText
}
// set axios config
var config = {
url:"https://ftx.us/api/wallet/all_balances",
method:"GET",
headers :{
"FTXUS-KEY":process.env.FTX_API_KEY,
"FTXUS-TS":time,
"FTXUS-SIGN":generateHash(`${new Date()}${"GET"}${"/wallet/all_balances"}`,process.env.FTX_API_SECRET)
}
}
axios(config)
.then(response => {
console.log(response.data)
}).catch(function (error) {
console.log(error);
})
I had to go through the same issue, so here goes my code.
import * as crypto from "crypto";
import fetch from "node-fetch";
// a function to call FTX (US)
async function callFtxAPIAsync(secrets, method, requestPath, body) {
const timestamp = Date.now();
const signaturePayload = timestamp + method.toUpperCase() + "/api" + requestPath + (method.toUpperCase() == "POST" ? JSON.stringify(body) : "");
const signature = crypto.createHmac('sha256', secrets.secret)
.update(signaturePayload)
.digest('hex');
const response = await fetch("https://ftx.us/api" + requestPath, {
method: method,
body: body != null ? JSON.stringify(body) : "",
headers: {
'FTXUS-KEY': secrets.key,
'FTXUS-TS': timestamp.toString(),
'FTXUS-SIGN': signature,
"Content-Type": "application/json",
"Accepts": "application/json"
}
});
return await response.json();
}
then call a post endpoint as for example:
let resultQuote = await callFtxAPIAsync(secrets, "post", "/otc/quotes",
{
"fromCoin": "USD",
"toCoin": "ETH",
"size": usd
});
or a get one:
let resultQuote = await callFtxAPIAsync(secrets, "get", "/otc/quotes/1234");
I hope it helps 😄
You need to add the full URL path, except the domain, in your case /api is missing. Try this:
"FTXUS-SIGN":generateHash(`${new Date()}${"GET"}${"/api/wallet/all_balances"}`,process.env.FTX_API_SECRET)

I cannot send base 64 url encoded data to my nodejs backend

I am trying to send post request to backend containing base64url encoded value of my image. When I send request with any random string, the request is received at backend but when I try to do same thing using encoded value, it responds me back with
request failed with status code 500
My code for the request is:
const uploadFileHandler = async (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
setPreviewSource(reader.result);
uploadtobackend(reader.result);
};
const uploadtobackend = async (filedata) => {
try {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
console.log(config);
console.log(filedata);
const { data } = await axios.post(
`/api/uploads`,
{
data: filedata,
},
config,
);
setImages(data);
setUploading(false);
} catch (error) {
console.log(error);
setUploading(false);
}
};
};
Here filedata is the encoded value and it is displayed if I console it.
Instead of
data: filedata
If I send
data: "some random string"
then request reaches backend otherwise not.
You nedd to send your file by wrapping in FormData.
You can do something like
const fd = new FormData()
// data you want to send other than file
fd.append('payload',JSON.stringify(payload))
fd.append('file',file) // file you want to send to node backend
Now in your backed you can get this file using req.file
Probably you need to use JSON.stringify() here are some example of use
const { data } = await axios.post(
`/api/uploads`,
{
data: new Buffer(JSON.stringify(filedata)).toString("base64")
},
config
)

Image uploaded to S3 becomes corrupted

I’m having issues uploading a file from postman to aws lambda + s3. If I understand correctly the image has to be a base64 string and send via JSON to work with lambda and API Gateway so I converted an image to a base64 and I’m using the base64 string in postman
The file uploads to S3, but when I download the s3 object and open it I get
So I don’t think I’m uploading it correctly. I’ve used a base64 to image converter and the image appears so the base64 string is correct before sending it via postman so something in my setup is off. What am I doing wrong? I appreciate the help!
upload.js
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event, context, callback) => {
let data = JSON.parse(event.body);
let file = data.base64String;
const s3Bucket = "upload-test3000";
const objectName = "helloworld.jpg";
const objectData = data.base64String;
const objectType = "image/jpg";
try {
const params = {
Bucket: s3Bucket,
Key: objectName,
Body: objectData,
ContentType: objectType
};
const result = await s3.putObject(params).promise();
return sendRes(200, `File uploaded successfully at https:/` + s3Bucket + `.s3.amazonaws.com/` + objectName);
} catch (error) {
return sendRes(404, error);
}
};
const sendRes = (status, body) => {
var response = {
statusCode: status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
"Access-Control-Allow-Methods": "OPTIONS,POST,PUT",
"Access-Control-Allow-Credentials": true,
"Access-Control-Allow-Origin": "*",
"X-Requested-With": "*"
},
body: body
};
return response;
};
.png
When building the params you should add content encoding, otherwise you're just uploading the text data:
const params = {
Bucket: s3Bucket,
Key: objectName,
Body: objectData,
ContentType: objectType,
ContentEncoding: 'base64'
};
edit
Okay I have checked the file, I think you might be misunderstanding what will happen when you store the image in base64.
Windows or a browser for that matter can't read a jpg file in base64 (as far as I know), it must be converted first. When you have an image in the browser with a base64 source, the browser handles this conversion on the fly but the base64 data inside the "helloworld.jpg" container is useless in windows without converting it.
There's two options, either convert once it reaches your server then upload directly as utf8 or have a layer in between, converting the image as it's requested.
The problem might be the passing format of image in Body, as it is not getting passed as expected by s3.upload parameters (It says Body key must be Buffer to be passed).
So, The simple solution is pass the Body as buffer, if your file is present in any location in the directory then don't pass it like
// Wrong Way
const params = {
Bucket: 'Your-Buket-Name',
Key: 'abc.png', // destFileName i.e the name of file to be saved in s3 bucket
Body: 'Path-To-File'
}
Now, the problem is the file gets uploaded as Raw Text, which is corrupted format and will not be readable by the OS on downloading.
So, to get it working pass it like
// Correct Way According to aws-sdk library
const fs = require('fs');
const imageData = fs.readFileSync('Path-To-File'); // returns buffer
const params = {
Bucket: 'Your-Buket-Name',
Key: 'abc.png', // destFileName i.e the name of file to be saved in s3 bucket
Body: imageData // image buffer
}
const uploadedFile = await s3.upload(params).promise();
Note: During answer i was using -> "aws-sdk": "^2.1025.0"
Hope this will help you or somebody else. Thanks!
I got it working by adding the base64 string in JSON format like so
and then sent
let decodedImage = Buffer.from(encodedImage, 'base64'); as the Body param.
updated upload.js
const AWS = require('aws-sdk');
var s3 = new AWS.S3();
exports.handler = async (event) => {
let encodedImage = JSON.parse(event.body).base64Data;
let decodedImage = Buffer.from(encodedImage, 'base64');
var filePath = "user-data/" + event.queryStringParameters.username + ".jpg"
var params = {
"Body": decodedImage,
"Bucket": process.env.UploadBucket,
"Key": filePath
};
try {
let uploadOutput = await s3.upload(params).promise();
let response = {
"statusCode": 200,
"body": JSON.stringify(uploadOutput),
"isBase64Encoded": false
};
return response;
}
catch (err) {
let response = {
"statusCode": 500,
"body": JSON.stringify(eerr),
"isBase64Encoded": false
};
return response;
}
};
I found this article to be super helpful

Coinbase web API through Node fetch "invalid API key" (not Coinbase pro)

Just trying to get my Coinbase balance. I have tried making a bunch of different API keys, keep getting the same error:
{
"errors": [{
"id": "authentication_error",
"message": "invalid api key"
}]
}
Im using Node.js through Netlify Lambda functions.
Here's my code:
import fetch from "node-fetch"
import crypto from "crypto"
const mykey = '<KEY>'
const mysecret = '<SECRET>'
exports.handler = async (event, context) => {
const url = `https://api.coinbase.com/v2/accounts`
var nonce = Math.floor(new Date().getTime() * 1e-3)
var my_hmac = crypto.createHmac('SHA256', nonce+'POST'+'v2/accounts', mysecret)
my_hmac.update(nonce + url)
var signature = my_hmac.digest('hex')
var msg;
return fetch(url, { headers:
{
'CB-ACCESS-KEY' : mykey,
'CB-ACCESS-SIGN': signature,
'CB-ACCESS-TIMESTAMP': nonce,
'Content-Type': 'application/json'
}
}).then(res => {
// console.log(res)
res.json
})
.then(data => {
return ({
statusCode: 200,
body: JSON.stringify(data)
})
})
}
You are using the wrong names for the tokens.
ACCESS_KEY is supposed to be CB-ACCESS-KEY
ACCESS_SIGNATURE is supposed to be CB-ACCESS-SIGN
I couldn't find info about the nonce. I found this over here.
Update:
signature looks like it is not made properly:
The nonce+'POST'+'/v2/accounts' is supposed to be the value in my_hmac.update
In turn for the createHmac it is only supposed to be SHA256 and mysecret
The signature pre-hash value is supposed to have a / at the beginning
A useful reference is
here (be sure to click node.js at the top).

gnupg encryption using nodejs

what is the better library to use gnupg encryption using nodejs
I have a binary public key,
Need to encrypt json payload,
Send it as a formdata (multipart-form) to another API.
I tried looking at openpgp.js, tried reading the key and encrypting, but no luck.
Appreciate any help
a binary public key was given to me along this article to encrypt and send a json payload.
https://www.ibm.com/support/knowledgecenter/SS4T5W/Watson_Talent_Data_Management_Administrator_Guide/gnupg_to_import_pgp_keys.html
I need to do this in nodejs..
I've used openpgp, form-data, stream for posting this as a formdata to api.
below is the code I've tried.
// encrypting the json payload. Is there anything I'm doing wrong here? receiving end is always failing to decrypt, always the response is a bad request
const pgpEncrypt = async (payload) => {
var encryptedKey = await fs.readFileSync("pub.bpg");
const keys = (await openpgp.key.read(encryptedKey)).keys;
const data = await openpgp.encrypt({
message: openpgp.message.fromText(JSON.stringify(payload, null, 0)),
publicKeys: keys,
armor: false,
});
return data.message.packets.write();
};
const formData = new FormData();
let fileContent = await pgpEncrypt(customPayload);
let stream = bufferToStream(Buffer.from(fileContent));
formData.append("file", stream);
const { Readable } = require("stream");
function bufferToStream(binary) {
const readableInstanceStream = new Readable({
read() {
this.push(binary);
this.push(null);
},
});
return readableInstanceStream;
}
axios.post(url, formData, {
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${accessToken}`,
},
})
);

Resources