A writeup on how I reversed engineered the puffco peak pro
In the U.S., Section 103(f) of the Digital Millennium Copyright Act (DMCA) (17 USC § 1201 (f) - Reverse Engineering) specifically states that it is legal to reverse engineer and circumvent the protection to achieve interoperability between computer programs (such as information transfer between applications). Interoperability is defined in paragraph 4 of Section 103(f).
It is also often lawful to reverse-engineer an artifact or process as long as it is obtained legitimately. If the software is patented, it doesn't necessarily need to be reverse-engineered, as patents require a public disclosure of invention. It should be mentioned that, just because a piece of software is patented, that does not mean the entire thing is patented; there may be parts that remain undisclosed.
Puffco, I love your products, and I mean no harm in releasing this information. I only did this as a side project so I can control the peak pro however I want. I decided to publish my findings so that anyone else who is looking to do the same has a place to start. Long story short, please don't sue me, or DMCA this repo. If you wish for me to take it down, please email me or leave a issue on this repo stating that you would like it to be removed, and I will happily do so.
Now on to the documentation/writeup!
Their new Firmware X update added a "Firmware Authentication", basically restricting you from reading/writing almost all characteristics. The way the app allows read/write is by taking an accessSeedKey from the puffco (which is different every time you connect) and doing a few things to it, down below you can find the steps on how their Authentication works for Firmware X.
- Read accessSeedKey from puffco E0 characteristic (as mention before, this is different everytime you connect)
- Create an empty 32 bit Uint8Array
- Add the hardcoded DEVICE_HANDSHAKE key to the first 16 bits of the empty Uint8Array
- Add the accessSeedKey to the last 16 bits of the Uint8Array
- Hash the Uint8Array with sha256 and convert the hex string to a num array
- Slice the 32 bit key (only keeping the first 16 bits)
- Write the new accessSeedKey to the E0 characteristic (The puffco is waiting for this new accessSeedKey, if its right you get read/write)
Below is the Authenticate function I deobfuscated from their webapp (includes some required functions as well):
const {createHash} = require('crypto');
function convertFromHex(hex) {//Thanks stackoverflow!
var hex = hex.toString();
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}
function convertHexStringToNumArray(h) {//From puffco's source
var i, j = (i = h.match(/.{2}/g)) != null ? i : [];
return j == null ? void 0x0 : j.map(function(k) {
return parseInt(k, 0x10);
});
}
var initialAccessSeedKey, DEVICE_HANDSHAKE_DECODED, newAccessSeedArray, newKeyIndex, newAccessSeedKeyHashed, finalAccessSeedKey;
initialAccessSeedKey = [42, 45, 124, 169, 105, 200, 18, 27, 188, 123, 188, 171, 2, 237, 37, 19]; //an AccessSeedKey I used while testing. You can get this from 'E0' characteristic. This is never the same and changes upon connecting.
DEVICE_HANDSHAKE_DECODED = convertFromHex(Buffer.from('FUrZc0WilhUBteT2JlCc+A==', 'base64').toString('hex'));//This DEVICE_HANDSHAKE is found by running the DEVICE_HANDSHAKE function found in their webapp
newAccessSeedArray = new Uint8Array(0x20); //Create 32bit Uint8Array
for (newKeyIndex = 0x0; newKeyIndex < 0x10; ++newKeyIndex) { //Loop, creating new 32bit key
newAccessSeedArray[newKeyIndex] = DEVICE_HANDSHAKE_DECODED.charCodeAt(newKeyIndex);//adding DEVICE_HANDSHAKE to first 16 bits
newAccessSeedArray[newKeyIndex + 0x10] = initialAccessSeedKey[newKeyIndex];//adding accessSeedKey to last 16 bits
}
newAccessSeedKeyHashed = convertHexStringToNumArray(createHash('sha256').update(newAccessSeedArray).digest('hex')); //hash to sha256 and convert the new AccessSeedKey to a num array
finalAccessSeedKey = newAccessSeedKeyHashed.slice(0x0, 0x10); //Slice and only use first 16 bits
console.log(finalAccessSeedKey); //Print new accessSeedKey
- Change lantern Colour.
- Send commands (preheat, boost, cycle profile)
- Change profiles (1-4) Colour, temperature, time.
- Set profile colour to any sort of animation.
- Set base, logo, main, glass LED brightness.
- Data not shown in the app.
- Dabs remaining until battery dies.
- Time until battery is charged.
- Total uptime.
- Total preheat cycle time.
- Device birthday.
- Knowing what every Bluetooth characteristic does.
- Found hidden features (New device, upcoming features).
- Their staging URL, which actually contained stuff never released.
- Their NEW Firmware X Authentication
I started reverse engineering with no plan in mind, I just wanted to poke around the Bluetooth characteristics and see what I could accomplish. I started off by using a GATT app on my phone, changing the values in the Puffco app and then seeing what changes in the GATT app. In the end I ended up extracting the main react native file from the android app and found out what every characteristic does. I will describe the characteristics I mainly looked into. At the end of this writeup will be the base UUID as well as the offsets.
When looking through the react native file, I found a lot of interesting things like their staging URL, every characteristic offset, new devices, upcoming app updates/features, and API URLS. The staging URL was quite interesting, when looking through it the first time I discovered chamber type values which contained the following (Classic, Herbal, Performance). This was interesting because at the time there was only one chamber type released (This was before the 3D was even announced). Recently there was a change to their staging URL showing the new LE-2 which was announced, just not show on Roger's live (I can say this LE is looking AMAZING) I will not post what it looks like in respect to the product team over at Puffco :).
Note: The following values are decimal not hexadecimal.
The order in which the bytes are will change depending on if rainbow is enabled or not.
Example:
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
#RED, GREEN, BLUE, RAINBOW, MODE, IDK, IDK, IDK
Example with rainbow:
0xFF, 0x1E, 0x00, 0x01, 0x00, 0x00, 0x00
#BRIGHTNESS, SPEED, MODE, RAINBOW, IDK, IDK, IDK
Modes i've found:
0x00 - Preserve?
0x01 - Static
0x05 - Breathing
0x06 - Rising
0x07 - Circling
0x15 - Circling Slow
This one was easy, its 4 bytes long and the first byte is either a 0 or a 1.
Turns off = 0x00, 0x00, 0x00, 0x00
Turns on = 0x01, 0x00, 0x00, 0x00
There's 4 segments to the brightness, each one goes to 255 and controls a separate LED.
0xFF, 0xFF, 0xFF, 0xFF
RING LED, UNDER GLASS LED, MAIN LED, BATTERY LED
There's only one set of offsets to control every profile value (temperature, time, colour, preheat colour, name) To change the selected profile, you need to set 0-3 to the heatCyclePointer characteristic (offset 61). This will change which profile is active in the app, not current selected profile on the peak.
Changing heatCyclePointer
0x00, 0x00, 0x00, 0x00 = Profile 1
0x01, 0x00, 0x00, 0x00 = Profile 2
0x02, 0x00, 0x00, 0x00 = Profile 3
0x03, 0x00, 0x00, 0x00 = Profile 4
Following are examples
heatCycleName (Offset 62)
- The profile name can be set as ASCII.
heatCycleTemp (Offset 64)
- 0x00, 0x80, 0x8F, 0x43 (Little Endian Float, value is 287 (Celsius))
heatCycleTime (Offset 64)
- 0x00, 0x00, 0xb8, 0x42 (Little Endian Float, value is 92 (Seconds))
heatCycleColor (Offset 65)
- 0x16, 0xE9, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00 (Greenish Colour)
heatCycleActiveColor (Offset 6B)
- 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 (This is being set to last colour I think)
It seems heatCyclePreheatColor doesn't work, ActiveColor seems to override it or something.
0 - Charging (0x00, 0x00, 0x00, 0x00)
2 - Done Charging (0x00, 0x00, 0x00, 0x40)
3 - Over Temp? (0x00, 0x00, 0x40, 0x40)
4 - Disconnected (0x00, 0x00, 0x80, 0x40)
Command List:
0 - master off (0x00, 0x00, 0x00, 0x00)
1 - sleep (0x00, 0x00, 0x80, 0x3F)
2 - idle (0x00, 0x00, 0x00, 0x40)
3 - tempSelectBegin (0x00, 0x00, 0x40, 0x40)
4 - tempSelectStop (0x00, 0x00, 128 0x40)
5 - showBatterylevel (0x00, 0x00, 0xA0, 0x40)
6 - showVersion (0x00, 0x00, 0xC0, 0x40)
7 - heatCycleStart (0x00, 0x00, 0xE0, 0x40)
8 - heatCycleAbort (0x00, 0x00, 0x00, 0x41)
9 - heatCycleBoost (0x00, 0x00, 0x10, 0x41)
10 - factoryTest (0x00, 0x00, 0x20, 0x41)
11 - bonding (0x00, 0x00, 0x30, 0x41)
This will change the selected profile on the device (Like clicking the button on the back to cycle through the profiles) UUID Offset is 41
Profile 1 - (0x00, 0x00, 0x00, 0x00)
Profile 2 - (0x00, 0x00, 0x80, 0x3F)
Profile 3 - (0x00, 0x00, 0x00, 0x40)
Profile 3 - (0x00, 0x00, 0x40, 0x40)
The base characteristic UUID is f9a98c15-c651-4f34-b656-d100bf5800
mfgDate: '00',
EUID: '01',
gitHash: '02',
batterySOC: '20',
batteryVoltage: '21',
operatingState: '22',
stateElapsedTime: '23',
stateTotalTime: '24',
heaterTemp: '25',
heaterTempCommand: '26',
activeLEDColor: '27',
heaterPower: '28',
heaterDuty: '29',
heaterVoltage: '2a',
heaterCurrent: '2b',
safetyThermalEstTemp: '2c',
heaterResistance: '2d',
batteryChargeCurrent: '2e',
totalHeatCycles: '2f',
totalHeatCycleTime: '30',
batteryChargeState: '31',
batterChargeElapseTime: '32',
batterChargeEstTimeToFull: '33',
batteryTemp: '34',
upTime: '35',
magneticField: '36',
inputCurrent: '37',
batteryCapacity: '38',
batteryCurrent: '39',
approxDabsRemaining: '3a',
dabsPerDay: '3b',
rawHeaterTemp: '3c',
rawHeaterTempCommand: '3d',
batteryChargeSource: '3e',
chamberType: '3f',
modeCommand: '40',
heatCycleSelect: '41',
stealthMode: '42',
deleteBondings: '43',
UTCTime: '44',
temperatureOverride: '45',
timeOverride: '46',
lanternPatternSetting: '47',
lanternColorSetting: '48',
lanternTimeSetting: '49',
lanternStart: '4a',
LEDbrightness: '4b',
readyModeCycleSelect: '4c',
deviceName: '4d',
deviceBirthday: '4e',
factoryReset: '4f',
terminateBondingAnimation: '50',
tripHeatCycles: '51',
tripHeatCycleTime: '52',
previewColor: '53',
heatCycleCount: '60',
heatCyclePointer: '61',
heatCycleName: '62',
heatCycleTemp: '63',
heatCycleTime: '64',
heatCycleColor: '65',
heatCyclePattern: '66',
heatCycleBoostTemp: '67',
heatCycleBoostTime: '68',
heatCycleThreshholdTemp: '69',
heatCyclePreheatColor: '6A',
heatCycleActiveColor: '6B',
auditLogPointer: 'c1',
auditLogEntry: 'c2',
auditLogBegin: 'c3',
auditLogEnd: 'c4',
faultLogPointer: 'd1',
faultLogEntry: 'd2',
faultLogBegin: 'd3',
faultLogEnd: 'd4'
accessSeedKey: 'e0'
pCoeff: 'e1'
iCoeff: 'e2'
dCoeff: 'e3'
dFilterTime: 'e4'
safetyThermalHeatCap: 'e5'
safetyThermalDecayTau: 'e6'
minWattSteadyStateFraction: 'e7'
nvmAccessPointer: '100'
nvmAccessData: '101'
Before we begin, puffco uses Gecko Bootloader by Silicon Labs (Not sure what board, I'm not focusing on hardware). Their firmware files have the extension .gbl
Device Name: '00002A00-0000-1000-8000-00805F9B34FB'
OTA Control Attribute: 'F7BF3564-FB6D-4E53-88A4-5E37E0326063'
OTA Data Attribute: '984227F3-34FC-4045-A5D0-2C581F81A153'
AppLoader Version: '4F4A2368-8CCA-451E-BFFF-CF0E2EE23E9F'
OTA Version: '4CC07BCF-0868-4B32-9DAD-BA4CC41E5316'
Gecko Bootloader Version: '25F05C0A-E917-46E9-B2A5-AA2BE1245AFE'
Application Version: '0D77CC11-4AC1-49F2-BFA9-CD96AC7A92F8'
The Puffco has 3 layers to it, the bootloader (Ignoring), the apploader (We will focus on this), and the application (We will focus on this also). The application itself is what handles everything from lighting effects to heating up your chamber. It is what the apploader loads, if there is no firmware (which is happens often when installing a new firmware), your puffco becomes unresponsive. At this time the puffco is actually stuck booting into apploader and WILL show a bluetooth device, device name is always the original mac address of when your puffco was in working state but the ':' are removed (Ex: B891901D38D4). Upon connecting, you don't need to provide any authentication you are free to read/write characteristics. An easy way I've found to check which firmware is installed is by reading the 'Application Version' Characteristic. If you see '00 00 00 00' then there is no firmware installed and you must install one. Below you can see what I've done to send a firmware file via bluetooth.
- Write '0x00' to OTA Control Attribute
- Start sending firmware 100 bytes at a time to OTA Data Attribute
- When finished, write '0x03' to OTA Control Attribute
Just after writing '0x03' to the OTA Control, it will parse the firmware and if the data was send correctly, your puffco should boot after several seconds. Note: If you want to boot the apploader (OTA DFU) then you can write '0x01' to the OTA Control Attribute. You will find this characteristic in the Application also
I've also wrote a firmware tool that lets you go from Firmware W to Firmware X and back freely, as many times as you'd like, it also detects puffcos with no firmware installed and will automatically install Firmware W for you. You can find an exe and the source over at https://github.com/Fr0st3h/Puffco-Firmware-Tool