Add bambu-nfc dump parse, Key-A HKDF, and spool merge.
Parse filanfc-dump/v1 zip/JSON, Bambu 1K images, and merge stickers by tray UID. dump and mifare packages stay importable for a later common library.
This commit is contained in:
+160
@@ -0,0 +1,160 @@
|
||||
// Package dump reads brand-blind filanfc-dump/v1 JSON and zip archives.
|
||||
package dump
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/mifare"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDump = errors.New("invalid dump")
|
||||
ErrImageSize = errors.New("image size")
|
||||
)
|
||||
|
||||
const FormatV1 = "filanfc-dump/v1"
|
||||
|
||||
// Dump is one sticker: chip UID plus a MIFARE Classic 1K image.
|
||||
type Dump struct {
|
||||
Format string
|
||||
ScannedAt time.Time
|
||||
ChipUID string
|
||||
SAK string
|
||||
ATQA string
|
||||
Tech []string
|
||||
SectorsOK []int
|
||||
SectorsFail []int
|
||||
Image []byte
|
||||
}
|
||||
|
||||
type fileDump struct {
|
||||
Format string `json:"format"`
|
||||
ScannedAt string `json:"scanned_at"`
|
||||
ChipUID string `json:"chip_uid"`
|
||||
SAK string `json:"sak"`
|
||||
ATQA string `json:"atqa"`
|
||||
Tech []string `json:"tech"`
|
||||
SectorsOK []int `json:"sectors_ok"`
|
||||
SectorsFail []int `json:"sectors_fail"`
|
||||
DumpHex string `json:"dump_hex"`
|
||||
DumpB64 string `json:"dump_b64"`
|
||||
}
|
||||
|
||||
// ParseJSON reads one filanfc-dump/v1 object.
|
||||
func ParseJSON(r io.Reader) (Dump, error) {
|
||||
var raw fileDump
|
||||
dec := json.NewDecoder(r)
|
||||
if err := dec.Decode(&raw); err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: %v", ErrInvalidDump, err)
|
||||
}
|
||||
return parseRaw(raw)
|
||||
}
|
||||
|
||||
func parseRaw(raw fileDump) (Dump, error) {
|
||||
if raw.Format != FormatV1 {
|
||||
return Dump{}, fmt.Errorf("%w: format %q", ErrInvalidDump, raw.Format)
|
||||
}
|
||||
uid := strings.ToLower(strings.TrimSpace(raw.ChipUID))
|
||||
if uid == "" || len(uid)%2 != 0 {
|
||||
return Dump{}, fmt.Errorf("%w: chip_uid %q", ErrInvalidDump, raw.ChipUID)
|
||||
}
|
||||
if _, err := hex.DecodeString(uid); err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: chip_uid: %v", ErrInvalidDump, err)
|
||||
}
|
||||
|
||||
var imgHex, imgB64 []byte
|
||||
var err error
|
||||
if raw.DumpHex != "" {
|
||||
imgHex, err = hex.DecodeString(raw.DumpHex)
|
||||
if err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: dump_hex: %v", ErrInvalidDump, err)
|
||||
}
|
||||
}
|
||||
if raw.DumpB64 != "" {
|
||||
imgB64, err = base64.StdEncoding.DecodeString(raw.DumpB64)
|
||||
if err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: dump_b64: %v", ErrInvalidDump, err)
|
||||
}
|
||||
}
|
||||
var image []byte
|
||||
switch {
|
||||
case len(imgHex) != 0 && len(imgB64) != 0:
|
||||
if !bytes.Equal(imgHex, imgB64) {
|
||||
return Dump{}, fmt.Errorf("%w: dump_hex and dump_b64 disagree", ErrInvalidDump)
|
||||
}
|
||||
image = imgHex
|
||||
case len(imgHex) != 0:
|
||||
image = imgHex
|
||||
case len(imgB64) != 0:
|
||||
image = imgB64
|
||||
default:
|
||||
return Dump{}, fmt.Errorf("%w: missing image", ErrInvalidDump)
|
||||
}
|
||||
if len(image) != mifare.Size1K {
|
||||
return Dump{}, fmt.Errorf("%w: want %d got %d", ErrImageSize, mifare.Size1K, len(image))
|
||||
}
|
||||
|
||||
var scanned time.Time
|
||||
if raw.ScannedAt != "" {
|
||||
scanned, err = time.Parse(time.RFC3339Nano, raw.ScannedAt)
|
||||
if err != nil {
|
||||
scanned, err = time.Parse(time.RFC3339, raw.ScannedAt)
|
||||
if err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: scanned_at: %v", ErrInvalidDump, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Dump{
|
||||
Format: raw.Format,
|
||||
ScannedAt: scanned,
|
||||
ChipUID: uid,
|
||||
SAK: raw.SAK,
|
||||
ATQA: raw.ATQA,
|
||||
Tech: raw.Tech,
|
||||
SectorsOK: raw.SectorsOK,
|
||||
SectorsFail: raw.SectorsFail,
|
||||
Image: image,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseZip reads every {chip_uid}.json entry in a dump zip.
|
||||
func ParseZip(r io.Reader) ([]Dump, error) {
|
||||
body, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: read zip: %v", ErrInvalidDump, err)
|
||||
}
|
||||
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: zip: %v", ErrInvalidDump, err)
|
||||
}
|
||||
var dumps []Dump
|
||||
for _, f := range zr.File {
|
||||
if !strings.HasSuffix(strings.ToLower(f.Name), ".json") {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: open %s: %v", ErrInvalidDump, f.Name, err)
|
||||
}
|
||||
d, err := ParseJSON(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidDump, f.Name, err)
|
||||
}
|
||||
dumps = append(dumps, d)
|
||||
}
|
||||
if len(dumps) == 0 {
|
||||
return nil, fmt.Errorf("%w: zip has no json entries", ErrInvalidDump)
|
||||
}
|
||||
return dumps, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package dump
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseJSONFixture(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, err := os.Open(filepath.Join("..", "testdata", "dumps", "e27be276.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
d, err := ParseJSON(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d.ChipUID != "e27be276" {
|
||||
t.Errorf("ChipUID = %q", d.ChipUID)
|
||||
}
|
||||
if d.Format != FormatV1 {
|
||||
t.Errorf("Format = %q", d.Format)
|
||||
}
|
||||
if len(d.Image) != 1024 {
|
||||
t.Errorf("Image len = %d", len(d.Image))
|
||||
}
|
||||
if len(d.SectorsFail) != 0 {
|
||||
t.Errorf("SectorsFail = %v", d.SectorsFail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseZipFixture(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, err := os.Open(filepath.Join("..", "testdata", "dumps.zip"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
dumps, err := ParseZip(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(dumps) != 6 {
|
||||
t.Errorf("len = %d; want 6", len(dumps))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONRejectsBadFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := ParseJSON(strings.NewReader(`{"format":"nope","chip_uid":"aa","dump_hex":"` + strings.Repeat("00", 1024) + `"}`))
|
||||
if !errors.Is(err, ErrInvalidDump) {
|
||||
t.Fatalf("err = %v; want ErrInvalidDump", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user