// 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 }