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  }