github.com/arduino/arduino-cloud-cli@v0.0.0-20240517070944-e7a449561083/internal/ota/decoder.go (about) 1 // This file is part of arduino-cloud-cli. 2 // 3 // Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published 7 // by the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 package ota 19 20 import ( 21 "bytes" 22 "crypto/sha256" 23 "encoding/binary" 24 "encoding/hex" 25 "fmt" 26 "hash/crc32" 27 "io" 28 "os" 29 "strconv" 30 "strings" 31 32 "github.com/arduino/arduino-cli/table" 33 "github.com/arduino/arduino-cloud-cli/internal/lzss" 34 ) 35 36 var ( 37 ErrCRC32Mismatch = fmt.Errorf("CRC32 mismatch") 38 ErrLengthMismatch = fmt.Errorf("file length mismatch") 39 40 boardTypes = map[uint32]string{ 41 0x45535033: "ESP32", 42 0x23418054: "MKR_WIFI_1010", 43 0x23418057: "NANO_33_IOT", 44 0x2341025B: "PORTENTA_H7_M7", 45 0x2341005E: "NANO_RP2040_CONNECT", 46 0x2341025F: "NICLA_VISION", 47 0x23410064: "OPTA", 48 0x23410266: "GIGA", 49 0x23410070: "NANO_ESP32", 50 0x23411002: "UNOR4WIFI", 51 } 52 53 arduinoPidToFQBN = map[string]string{ 54 "8057": "arduino:samd:nano_33_iot", 55 "804E": "arduino:samd:mkr1000", 56 "8052": "arduino:samd:mkrgsm1400", 57 "8055": "arduino:samd:mkrnb1500", 58 "8054": "arduino:samd:mkrwifi1010", 59 "005E": "arduino:mbed_nano:nanorp2040connect", 60 "025B": "arduino:mbed_portenta:envie_m7", 61 "025F": "arduino:mbed_nicla:nicla_vision", 62 "0064": "arduino:mbed_opta:opta", 63 "0266": "arduino:mbed_giga:giga", 64 "0070": "arduino:esp32:nano_nora", 65 "1002": "arduino:renesas_uno:unor4wifi", 66 } 67 ) 68 69 const ( 70 OffsetLength = 0 71 OffsetCRC32 = 4 72 OffsetMagicNumber = 8 73 OffsetVersion = 12 74 OffsetPayload = 20 75 HeaderSize = 20 76 ) 77 78 type OtaFileReader interface { 79 io.Reader 80 io.Closer 81 } 82 83 type OtaMetadata struct { 84 Length uint32 85 CRC32 uint32 86 MagicNumber uint32 87 BoardType string 88 FQBN *string 89 VID string 90 PID string 91 IsArduinoBoard bool 92 Compressed bool 93 PayloadSHA256 string // SHA256 of the payload (decompressed if compressed). This is the SHA256 as seen ny the board. 94 OtaSHA256 string // SHA256 of the whole file (header + payload). 95 } 96 97 func (r OtaMetadata) Data() interface{} { 98 return r 99 } 100 101 func (r OtaMetadata) String() string { 102 t := table.New() 103 104 t.SetHeader("Entry", "Value") 105 106 t.AddRow([]interface{}{"Length", fmt.Sprintf("%d bytes", r.Length)}...) 107 t.AddRow([]interface{}{"CRC32", fmt.Sprintf("%d", r.CRC32)}...) 108 t.AddRow([]interface{}{"Magic Number", fmt.Sprintf("0x%08X", r.MagicNumber)}...) 109 t.AddRow([]interface{}{"Board Type", r.BoardType}...) 110 if r.FQBN != nil { 111 t.AddRow([]interface{}{"FQBN", *r.FQBN}...) 112 } 113 t.AddRow([]interface{}{"VID", r.VID}...) 114 t.AddRow([]interface{}{"PID", r.PID}...) 115 t.AddRow([]interface{}{"Is Arduino Board", strconv.FormatBool(r.IsArduinoBoard)}...) 116 t.AddRow([]interface{}{"Compressed", strconv.FormatBool(r.Compressed)}...) 117 t.AddRow([]interface{}{"Payload SHA256", r.PayloadSHA256}...) 118 t.AddRow([]interface{}{"OTA SHA256", r.OtaSHA256}...) 119 120 return t.Render() 121 } 122 123 // Read header starting from the first byte of the file 124 func readHeader(file OtaFileReader) ([]byte, error) { 125 bytes := make([]byte, HeaderSize) 126 _, err := file.Read(bytes) 127 if err != nil { 128 return nil, err 129 } 130 return bytes, nil 131 } 132 133 // Function will compute OTA CRC32 and file SHA256 hash, starting from a reader that has already extracted the header (so pointing to payload). 134 func computeFileHashes(file OtaFileReader, compressed bool, otaHeader []byte) (uint32, string, string, uint32, error) { 135 crcSum := crc32.NewIEEE() 136 payload := bytes.Buffer{} 137 // Length of remaining header + payload excluding the fields LENGTH and CRC32. 138 computedLength := HeaderSize - OffsetMagicNumber 139 140 // Discard first 8 bytes (len + crc32) and read remaining header's bytes (12B - magic number + version) 141 crcSum.Write(otaHeader[8:HeaderSize]) 142 143 // Read file in chunks and compute CRC32. Save payload in a buffer for next processing steps (SHA256) 144 buf := make([]byte, 4096) 145 for { 146 n, err := file.Read(buf) 147 if err != nil && err != io.EOF { 148 return 0, "", "", 0, err 149 } 150 if n == 0 { 151 break 152 } 153 computedLength += n 154 crcSum.Write(buf[:n]) 155 payload.Write(buf[:n]) 156 } 157 158 payloadSHA, otaSHA := computeBinarySha256(compressed, payload.Bytes(), otaHeader) 159 160 return crcSum.Sum32(), payloadSHA, otaSHA, uint32(computedLength), nil 161 } 162 163 func computeBinarySha256(compressed bool, payload []byte, otaHeader []byte) (string, string) { 164 var computedShaBytes [32]byte 165 if compressed { 166 decompressed := lzss.Decompress(payload) 167 computedShaBytes = sha256.Sum256(decompressed) 168 } else { 169 computedShaBytes = sha256.Sum256(payload) 170 } 171 172 // Whole file SHA256 (header + payload) 173 otaSHA := sha256.New() 174 otaSHA.Write(otaHeader) 175 otaSHA.Write(payload) 176 177 return hex.EncodeToString(computedShaBytes[:]), hex.EncodeToString(otaSHA.Sum(nil)) 178 } 179 180 func extractXID(buff []byte) string { 181 xid := strconv.FormatUint(uint64(binary.LittleEndian.Uint16(buff)), 16) 182 return strings.ToUpper(xid) 183 } 184 185 // DecodeOtaFirmwareHeader decodes the OTA firmware header from a binary file. 186 // File is composed by an header and a payload (optionally lzss compressed). 187 // Method is also checking CRC32 of the file, verifying that file is not corrupted. 188 // OTA header layout: LENGTH (4 B) | CRC (4 B) | MAGIC NUMBER = VID + PID (4 B) | VERSION (8 B) | PAYLOAD (LENGTH - 12 B) 189 // See https://arduino.atlassian.net/wiki/spaces/RFC/pages/1616871540/OTA+header+structure 190 func DecodeOtaFirmwareHeaderFromFile(binaryFilePath string) (*OtaMetadata, error) { 191 // Check if file exists 192 if _, err := os.Stat(binaryFilePath); err != nil { 193 return nil, err 194 } 195 if otafileptr, err := os.Open(binaryFilePath); err != nil { 196 return nil, err 197 } else { 198 defer otafileptr.Close() 199 return DecodeOtaFirmwareHeader(otafileptr) 200 } 201 } 202 203 // DecodeOtaFirmwareHeader decodes the OTA firmware header from a binary file. 204 // File is composed by an header and a payload (optionally lzss compressed). 205 // Method is also checking CRC32 of the file, verifying that file is not corrupted. 206 // OTA header layout: LENGTH (4 B) | CRC (4 B) | MAGIC NUMBER = VID + PID (4 B) | VERSION (8 B) | PAYLOAD (LENGTH - 12 B) 207 // See https://arduino.atlassian.net/wiki/spaces/RFC/pages/1616871540/OTA+header+structure 208 func DecodeOtaFirmwareHeader(otafileptr OtaFileReader) (*OtaMetadata, error) { 209 header, err := readHeader(otafileptr) // Read all header. 210 if err != nil { 211 return nil, err 212 } 213 214 // Get length (payload + header without length and CRC32 bytes) 215 lengthInt := binary.LittleEndian.Uint32(header[OffsetLength:OffsetCRC32]) 216 217 // Get CRC32 (uint32) 218 readsum := binary.LittleEndian.Uint32(header[OffsetCRC32:OffsetMagicNumber]) 219 220 // Get PID+VID (uint32) 221 completeMagicNumber := binary.LittleEndian.Uint32(header[OffsetMagicNumber:OffsetVersion]) 222 223 //Extract PID and VID. VID is in the last 2 bytes of the magic number, PID in the first 2 bytes. 224 pid := extractXID(header[OffsetMagicNumber : OffsetMagicNumber+2]) 225 vid := extractXID(header[OffsetMagicNumber+2 : OffsetVersion]) 226 227 boardType, fqbn, isArduino := getBoardType(completeMagicNumber, pid) 228 229 // Get Version (8B) 230 version := decodeVersion(header[OffsetVersion:OffsetPayload]) 231 232 // Read full binary file (buffered), starting from 8th byte (magic number) 233 computedsum, fileSha, otaSha, computedLength, err := computeFileHashes(otafileptr, version.Compression, header) 234 if err != nil { 235 return nil, err 236 } 237 238 // File sanity check. Validate CRC32 and length declared in header with computed values. 239 if computedsum != readsum { 240 return nil, ErrCRC32Mismatch 241 } 242 if computedLength != lengthInt { 243 return nil, ErrLengthMismatch 244 } 245 246 return &OtaMetadata{ 247 Length: lengthInt, 248 CRC32: computedsum, 249 BoardType: boardType, 250 MagicNumber: completeMagicNumber, 251 IsArduinoBoard: isArduino, 252 PID: pid, 253 VID: vid, 254 FQBN: fqbn, 255 Compressed: version.Compression, 256 PayloadSHA256: fileSha, 257 OtaSHA256: otaSha, 258 }, nil 259 } 260 261 func getBoardType(magicNumber uint32, pid string) (string, *string, bool) { 262 baordType := "UNKNOWN" 263 if t, ok := boardTypes[magicNumber]; ok { 264 baordType = t 265 } 266 isArduino := baordType != "UNKNOWN" && baordType != "ESP32" 267 var fqbn *string 268 if t, ok := arduinoPidToFQBN[pid]; ok { 269 fqbn = &t 270 } 271 272 return baordType, fqbn, isArduino 273 }