To increase our understanding of the transaction that we just created. Let's inspect its JSON-decoded form. A bitcoin transaction is often serialized in hexadecimal format. This hexadecimal format can be parsed and represented as a JSON object. Blockchair's API will return the JSON represenation of the transaction in the data.<txid>.decoded_raw_transaction field.
Take a closer look at decoded_raw_transaction.vout[0].scriptPubKey.asm. This is a Bitcoin Script and it contains an assembly language that is executed by miners in order to verify that a specific output can be spent. In this case, the output is unspendable and simply contains data. Let's dissect this script:
Now that we understand how a Pigi binary blob is stored within a transaction, lets parse the Pigi binary blob and decrypt its contents.
Read the first byte (version)
Read the second byte (algorithm)
Read the next 32 bytes (file id)
Read the next 4 bytes (length)
Read the next 16 bytes (IV)
Read the next encryptedSize bytes (encryptedData)
Part 2: Decrypting AES-256-CBC
The upload reponse returned a secret key inside the identifiers.raw.secret field. In our case: WmH5w6eCzNrYSAc80Hgtd2GZq8qwAtt6PS3Lct6yzzA0
This is a base62-encoded representation of a 32 byte secret key. To convert the base62 key to binary, use any base62 library and use the following alphabet:
Now that the key is in binary form. We can decrypt encryptedData as follows:
JS
|
decryptedData =AES256CBC-DECRYPT(encryptedData, iv, secret);
ο»Ώ
Part 3: Decompressing
The decrypted data is an LZMA compressed binary blob. We must now decompress it
JS
|
decompressedData =LZMA-DECOMPRESS(decryptedData);
ο»Ώ
Part 4: Decoding CBOR
The decompressed data contains a CBOR encoded array. We must use CBOR to decode this decompressed data. After we have CBOR decoded the decompressed data, we have access to the filename, file type and file contents.
/**
* This file contains helper functions to assemble and parse a Pigi binary message
*/import crypto from'crypto';import r from'restructure';import base62 from'@fry/base62';import cbor from'cbor';// eslint-disable-next-line @typescript-eslint/no-var-requiresconst lzma =require('lzma-native');exportconst Algorithms =['aes-256-cbc:lzma:cbor'];exportconst PigiStruct =newr.VersionedStruct(r.uint8,{0:{algorithm:newr.Enum(r.uint8, Algorithms),originalFileHash:newr.Buffer(32),encryptedSize: r.uint32be,iv:newr.Buffer(16),encryptedData:newr.Buffer('encryptedSize'),},});exportinterfacePigi{version: number;algorithm: string;originalFileHash: Buffer;encryptedSize: number;iv: Buffer;encryptedData: Buffer;_opReturnChunk: Buffer;_secret: Buffer;_secretHex: string;_secretBase62: string;_id: Buffer;_idHex: string;_idBase62: string;_algorithmIndex: number;_mimetype: string;_filename: string;_plainData: Buffer;_originalSize: number;}// This is the request handler to look up a file by txid and secretexportasyncfunctionlookupByTxid(req: express.Request,res: express.Response): Promise<void>{try{// Get the txid from the request parameterconst txid = req.params.txid;// Get the base62 secet from the query parameterconst s = req.query.s;// Decode the base62 secret to binaryconst secret = base62.decode(s).slice(0,32);// Fetch the raw transaction from WhatsOnChainconst rawtx =await axios.get(`https://api.whatsonchain.com/v1/bsv/main/tx/${txid}/hex`).then((res)=> res.data);// Parse the raw transactionconst tx =newbsv.Transaction(rawtx);// Take the first output, and of that, the last script chunk (pigi binary blob)const pigiBuffer = tx.outputs[0].script.chunks[tx.outputs[0].script.chunks.length -1].buf;// Decode the pigi binary blob (function is defined below)const p =awaitdecodeOpReturn(pigiBuffer, secret);setDownloadResponseHeaders(p, res);
res.setHeader('Pigi-Bitcoin-TxId', tx.hash);// The raw file contents is now in p._plainData
res.send(p._plainData);}catch(e){
res.status(404).json({error: e.message,stack: e.stack });}}exportasyncfunctiondecodeOpReturn(opReturnChunk: Buffer,secret: Buffer): Promise<Pigi>{// Parse the pigi binary blob into its fields.const stream =newr.DecodeStream(opReturnChunk);const pigi = PigiStruct.decode(stream);const cipher = pigi.algorithm.split(':')[0];// aes-256-cbcconst iv = pigi.iv;// decryptconst decipher = crypto.createDecipheriv(cipher, secret, iv);let decrypt = decipher.update(pigi.encryptedData);
decrypt = Buffer.concat([decrypt, decipher.final()]);// decompressconst decompressed =await lzma.decompress(decrypt);// cbor decodeconst d = cbor.decode(decompressed);const filename = d[0];const mimetype = d[1];const filebuf = d[2];return{version: pigi.version,algorithm: pigi.algorithm,originalFileHash: pigi.originalFileHash,encryptedSize: pigi.encryptedSize,iv: pigi.iv,encryptedData: pigi.encryptedData,_algorithmIndex:0,_filename: filename,_id: pigi.hashOriginalFile,_idBase62: base62.encode(pigi.hashOriginalFile),_idHex: pigi.originalFileHash.toString('hex'),_secret: secret,_secretHex: secret.toString('hex'),_secretBase62: base62.encode(secret),_mimetype: mimetype,_opReturnChunk: opReturnChunk,_plainData: filebuf,_originalSize: filebuf.length,};}