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:
2026-08-23 15:23:11 -05:00
parent a794db4838
commit b38f7f643b
29 changed files with 893 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# bambu-nfc
Go library for Bambu Lab filament NFC dumps produced by the Filament NFC dump Android app.
```go
import bambunfc "git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc"
tags, err := bambunfc.ParseZip(r)
spools, err := bambunfc.Merge(tags)
keys, err := bambunfc.KeysA(uid)
```
- `dump` — brand-blind `filanfc-dump/v1` JSON/zip
- `mifare` — Classic 1K layout helpers
- root package — Bambu Key-A HKDF, block parse, merge by `tray_uid`
`Brand` is `"Bambu Lab"` on a successful parse.
```bash
go test -race ./...
```
+13
View File
@@ -0,0 +1,13 @@
package bambunfc
import "fmt"
// Color is an RGBA filament color from a Bambu tag.
type Color struct {
R, G, B, A uint8
}
// Hex returns "#RRGGBBAA" with uppercase hex digits.
func (c Color) Hex() string {
return fmt.Sprintf("#%02X%02X%02X%02X", c.R, c.G, c.B, c.A)
}
+2
View File
@@ -0,0 +1,2 @@
// Package bambunfc parses Bambu Lab filament NFC dumps and derives MIFARE Key-A from a chip UID.
package bambunfc
@@ -0,0 +1,23 @@
# bambu-nfc Library Implementation Plan
> **For agentic workers:** Use TDD. Spec: `docs/superpowers/specs/2026-08-23-bambu-nfc-library-design.md`
**Goal:** Go module that parses filanfc-dump/v1 zips into Bambu tags/spools and derives MIFARE Key-A from a chip UID.
**Architecture:** Public packages `dump` (JSON/zip), `mifare` (1K layout), root `bambunfc` (HKDF, block parse, Merge).
**Tech Stack:** Go 1.25, stdlib `crypto/hkdf`, `encoding/json`, `archive/zip`.
## Global Constraints
- Module `git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc`
- Brand on success: `"Bambu Lab"`
- HKDF: IKM=UID, salt=`9a759cf2c4f7caff222cb9769b41bc96`, info=`RFID-A\0`, 16×6 Key-A
- Diameter float32 at block 5 offset 8; tray UID 16 raw bytes hex
- Required dump sectors for a Bambu parse: 0, 1, 2 (`sectors_fail` is sector indexes)
- `go test -race ./...`
- Fixtures from dump-app `docs/dumps/``testdata/`
Tasks: (1) module + mifare (2) dump JSON/zip (3) KeysA (4) ParseImage/ParseDump/ParseZip (5) Merge (6) testdata + README.
Executing in this session per user "do it".
+160
View File
@@ -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
}
+58
View File
@@ -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)
}
}
+12
View File
@@ -0,0 +1,12 @@
package bambunfc
import "errors"
var (
ErrInvalidDump = errors.New("invalid dump")
ErrImageSize = errors.New("image size")
ErrIncomplete = errors.New("incomplete tag")
ErrNotBambu = errors.New("not bambu")
ErrUID = errors.New("uid")
ErrConflict = errors.New("tag conflict")
)
+3
View File
@@ -0,0 +1,3 @@
module git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc
go 1.25
+30
View File
@@ -0,0 +1,30 @@
package bambunfc
import (
"crypto/hkdf"
"crypto/sha256"
"fmt"
)
var kdfSalt = []byte{
0x9a, 0x75, 0x9c, 0xf2, 0xc4, 0xf7, 0xca, 0xff,
0x22, 0x2c, 0xb9, 0x76, 0x9b, 0x41, 0xbc, 0x96,
}
const kdfInfo = "RFID-A\x00"
// KeysA derives the 16 MIFARE Key-A values for a Bambu tag from its chip UID.
func KeysA(uid []byte) ([16][6]byte, error) {
var keys [16][6]byte
if len(uid) == 0 {
return keys, fmt.Errorf("%w: empty", ErrUID)
}
okm, err := hkdf.Key(sha256.New, uid, kdfSalt, kdfInfo, 16*6)
if err != nil {
return keys, fmt.Errorf("%w: hkdf: %v", ErrUID, err)
}
for i := range 16 {
copy(keys[i][:], okm[i*6:i*6+6])
}
return keys, nil
}
+48
View File
@@ -0,0 +1,48 @@
package bambunfc
import (
"encoding/hex"
"errors"
"testing"
)
func TestKeysAGolden11223344(t *testing.T) {
t.Parallel()
uid, err := hex.DecodeString("11223344")
if err != nil {
t.Fatal(err)
}
want := []string{
"0729F3B2D37A", "2027210D85E7", "D77B2E7C92BC", "29126A53A2EE",
"E49850F62778", "9A9128B882BD", "45F69B980786", "90DD67095D6B",
"EB994746CD69", "B4AEB23473E3", "445E850D699C", "5C9BD5BD9FD7",
"1062AFE9F5F5", "81EE5259CF6B", "0920CEEEE2BC", "BAF630CCBFD3",
}
got, err := KeysA(uid)
if err != nil {
t.Fatal(err)
}
for i, w := range want {
if h := hex.EncodeToString(got[i][:]); h != toLower(w) {
t.Errorf("key[%d] = %s; want %s", i, h, toLower(w))
}
}
}
func toLower(s string) string {
b := []byte(s)
for i, c := range b {
if c >= 'A' && c <= 'F' {
b[i] = c + ('a' - 'A')
}
}
return string(b)
}
func TestKeysAEmpty(t *testing.T) {
t.Parallel()
_, err := KeysA(nil)
if !errors.Is(err, ErrUID) {
t.Errorf("KeysA(nil) err = %v; want ErrUID", err)
}
}
+107
View File
@@ -0,0 +1,107 @@
package bambunfc
import (
"fmt"
"slices"
)
// Spool is one physical filament roll (both stickers merged).
type Spool struct {
Brand string
TrayUID string
ChipUIDs []string
Type string
Material string
MaterialID string
VariantID string
Color Color
WeightG int
DiameterMM float32
LengthM int
MinHotendC int
MaxHotendC int
BedC int
DryC int
DryHours int
Produced string
SpoolWidthRaw uint16
}
func spoolKey(t Tag) string {
return t.Brand + "\x00" + t.TrayUID
}
func sameFilament(a, b Tag) bool {
return a.Type == b.Type &&
a.Material == b.Material &&
a.Color == b.Color &&
a.WeightG == b.WeightG
}
func tagToSpool(t Tag) Spool {
return Spool{
Brand: t.Brand,
TrayUID: t.TrayUID,
ChipUIDs: []string{t.ChipUID},
Type: t.Type,
Material: t.Material,
MaterialID: t.MaterialID,
VariantID: t.VariantID,
Color: t.Color,
WeightG: t.WeightG,
DiameterMM: t.DiameterMM,
LengthM: t.LengthM,
MinHotendC: t.MinHotendC,
MaxHotendC: t.MaxHotendC,
BedC: t.BedC,
DryC: t.DryC,
DryHours: t.DryHours,
Produced: t.Produced,
SpoolWidthRaw: t.SpoolWidthRaw,
}
}
// Merge groups tags by brand + tray UID into spools.
func Merge(tags []Tag) ([]Spool, error) {
order := make([]string, 0)
groups := make(map[string][]Tag)
for _, t := range tags {
if t.TrayUID == "" {
return nil, fmt.Errorf("%w: empty tray uid chip %s", ErrIncomplete, t.ChipUID)
}
k := spoolKey(t)
if _, ok := groups[k]; !ok {
order = append(order, k)
}
groups[k] = append(groups[k], t)
}
spools := make([]Spool, 0, len(order))
for _, k := range order {
g := groups[k]
s := tagToSpool(g[0])
uids := map[string]struct{}{g[0].ChipUID: {}}
for _, t := range g[1:] {
if !sameFilament(g[0], t) {
return nil, fmt.Errorf("%w: tray %s", ErrConflict, t.TrayUID)
}
uids[t.ChipUID] = struct{}{}
}
s.ChipUIDs = make([]string, 0, len(uids))
for u := range uids {
s.ChipUIDs = append(s.ChipUIDs, u)
}
slices.Sort(s.ChipUIDs)
spools = append(spools, s)
}
slices.SortFunc(spools, func(a, b Spool) int {
if a.TrayUID < b.TrayUID {
return -1
}
if a.TrayUID > b.TrayUID {
return 1
}
return 0
})
return spools, nil
}
+87
View File
@@ -0,0 +1,87 @@
package bambunfc
import (
"errors"
"os"
"slices"
"testing"
)
func TestMergeFixtures(t *testing.T) {
t.Parallel()
f, err := os.Open("testdata/dumps.zip")
if err != nil {
t.Fatal(err)
}
defer f.Close()
tags, err := ParseZip(f)
if err != nil {
t.Fatal(err)
}
spools, err := Merge(tags)
if err != nil {
t.Fatal(err)
}
if len(spools) != 3 {
t.Fatalf("len(spools) = %d; want 3", len(spools))
}
byTray := map[string]Spool{}
for _, s := range spools {
byTray[s.TrayUID] = s
}
a, ok := byTray["2fb8e0e972084e74bf737393b28e7b12"]
if !ok {
t.Fatal("missing spool A")
}
if !slices.Equal(a.ChipUIDs, []string{"52d60177", "e27be276"}) {
t.Errorf("spool A chips %v", a.ChipUIDs)
}
if a.Type != "PETG Translucent" || a.Brand != BrandBambuLab {
t.Errorf("spool A type/brand %s %s", a.Type, a.Brand)
}
b, ok := byTray["aa42f322172c48e99c0ece895346ea9f"]
if !ok {
t.Fatal("missing spool B")
}
if !slices.Equal(b.ChipUIDs, []string{"92a80577", "b2d32177"}) {
t.Errorf("spool B chips %v", b.ChipUIDs)
}
c, ok := byTray["3cb568b7af4f41819412fe60afc5c446"]
if !ok {
t.Fatal("missing spool C")
}
if !slices.Equal(c.ChipUIDs, []string{"92b6fb31", "f28c58ed"}) {
t.Errorf("spool C chips %v", c.ChipUIDs)
}
if c.Type != "PETG Basic" || c.Color.Hex() != "#000000FF" {
t.Errorf("spool C %s %s", c.Type, c.Color.Hex())
}
for i := 1; i < len(spools); i++ {
if spools[i-1].TrayUID > spools[i].TrayUID {
t.Fatalf("spools not sorted by TrayUID")
}
}
}
func TestMergeConflict(t *testing.T) {
t.Parallel()
a := Tag{Brand: BrandBambuLab, TrayUID: "aa", ChipUID: "01", Type: "PLA", Material: "PLA", WeightG: 1000}
b := Tag{Brand: BrandBambuLab, TrayUID: "aa", ChipUID: "02", Type: "PETG", Material: "PETG", WeightG: 1000}
_, err := Merge([]Tag{a, b})
if !errors.Is(err, ErrConflict) {
t.Errorf("err = %v; want ErrConflict", err)
}
}
func TestMergeEmptyTray(t *testing.T) {
t.Parallel()
_, err := Merge([]Tag{{Brand: BrandBambuLab, ChipUID: "01", Type: "PLA"}})
if !errors.Is(err, ErrIncomplete) {
t.Errorf("err = %v; want ErrIncomplete", err)
}
}
+36
View File
@@ -0,0 +1,36 @@
// Package mifare holds MIFARE Classic 1K layout helpers with no brand-specific memory map.
package mifare
import "fmt"
const (
Sectors = 16
Blocks = 64
BlockSize = 16
SectorSize = 64
Size1K = 1024
)
// SectorOf returns the sector index for a block number (063).
func SectorOf(block int) int {
return block / 4
}
// BlockRange returns the half-open block range [first, last) for a sector.
func BlockRange(sector int) (first, last int) {
first = sector * 4
last = first + 4
return first, last
}
// SectorBytes returns the 64-byte slice for sector (015) from a 1K image.
func SectorBytes(image []byte, sector int) ([]byte, error) {
if len(image) != Size1K {
return nil, fmt.Errorf("image size: want %d got %d", Size1K, len(image))
}
if sector < 0 || sector >= Sectors {
return nil, fmt.Errorf("sector %d out of range", sector)
}
off := sector * SectorSize
return image[off : off+SectorSize], nil
}
+30
View File
@@ -0,0 +1,30 @@
package mifare
import "testing"
func TestSectorOfAndBlockRange(t *testing.T) {
t.Parallel()
if got := SectorOf(9); got != 2 {
t.Errorf("SectorOf(9) = %d; want 2", got)
}
first, last := BlockRange(2)
if first != 8 || last != 12 {
t.Errorf("BlockRange(2) = %d,%d; want 8,12", first, last)
}
}
func TestSectorBytes(t *testing.T) {
t.Parallel()
img := make([]byte, Size1K)
img[2*SectorSize] = 0xAB
got, err := SectorBytes(img, 2)
if err != nil {
t.Fatal(err)
}
if got[0] != 0xAB || len(got) != SectorSize {
t.Errorf("SectorBytes sector 2: len=%d first=%02x", len(got), got[0])
}
if _, err := SectorBytes(img[:1023], 0); err == nil {
t.Fatal("short image: want error")
}
}
+156
View File
@@ -0,0 +1,156 @@
package bambunfc
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"math"
"strings"
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/dump"
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/mifare"
)
const BrandBambuLab = "Bambu Lab"
// requiredSectors must not appear in SectorsFail (blocks 02, 46, 89).
var requiredSectors = []int{0, 1, 2}
// Tag is one Bambu sticker after parsing the 1K image.
type Tag struct {
Brand string
ChipUID string
TrayUID string
Type string
Material string
MaterialID string
VariantID string
Color Color
WeightG int
DiameterMM float32
LengthM int
MinHotendC int
MaxHotendC int
BedC int
DryC int
DryHours int
Produced string
SpoolWidthRaw uint16
}
func failedSet(fail []int) map[int]bool {
s := make(map[int]bool, len(fail))
for _, n := range fail {
s[n] = true
}
return s
}
func block(image []byte, n int) []byte {
off := n * mifare.BlockSize
return image[off : off+mifare.BlockSize]
}
func cstr(b []byte) string {
if i := bytes.IndexByte(b, 0); i >= 0 {
b = b[:i]
}
return string(b)
}
func normalizeUID(uid string) (string, error) {
uid = strings.ToLower(strings.TrimSpace(uid))
if uid == "" || len(uid)%2 != 0 {
return "", fmt.Errorf("%w: %q", ErrUID, uid)
}
if _, err := hex.DecodeString(uid); err != nil {
return "", fmt.Errorf("%w: %v", ErrUID, err)
}
return uid, nil
}
// ParseImage parses a Bambu MIFARE Classic 1K image.
// fail is the dump's sectors_fail list (sector indexes 015).
func ParseImage(chipUID string, image []byte, fail []int) (Tag, error) {
var zero Tag
uid, err := normalizeUID(chipUID)
if err != nil {
return zero, err
}
if len(image) != mifare.Size1K {
return zero, fmt.Errorf("%w: want %d got %d", ErrImageSize, mifare.Size1K, len(image))
}
failed := failedSet(fail)
for _, s := range requiredSectors {
if failed[s] {
return zero, fmt.Errorf("%w: sector %d", ErrIncomplete, s)
}
}
b1 := block(image, 1)
b2 := block(image, 2)
b4 := block(image, 4)
b5 := block(image, 5)
b6 := block(image, 6)
b9 := block(image, 9)
material := cstr(b2)
typ := cstr(b4)
if material == "" && typ == "" {
return zero, ErrNotBambu
}
tag := Tag{
Brand: BrandBambuLab,
ChipUID: uid,
VariantID: cstr(b1[0:8]),
MaterialID: cstr(b1[8:16]),
Material: material,
Type: typ,
Color: Color{
R: b5[0], G: b5[1], B: b5[2], A: b5[3],
},
WeightG: int(binary.LittleEndian.Uint16(b5[4:6])),
DiameterMM: math.Float32frombits(binary.LittleEndian.Uint32(b5[8:12])),
TrayUID: hex.EncodeToString(b9),
DryC: int(binary.LittleEndian.Uint16(b6[0:2])),
DryHours: int(binary.LittleEndian.Uint16(b6[2:4])),
BedC: int(binary.LittleEndian.Uint16(b6[6:8])),
MaxHotendC: int(binary.LittleEndian.Uint16(b6[8:10])),
MinHotendC: int(binary.LittleEndian.Uint16(b6[10:12])),
}
if !failed[2] {
b10 := block(image, 10)
tag.SpoolWidthRaw = binary.LittleEndian.Uint16(b10[4:6])
}
if !failed[3] {
tag.Produced = cstr(block(image, 12))
b14 := block(image, 14)
tag.LengthM = int(binary.LittleEndian.Uint16(b14[4:6]))
}
return tag, nil
}
// ParseDump parses one dump JSON record as a Bambu tag.
func ParseDump(d dump.Dump) (Tag, error) {
return ParseImage(d.ChipUID, d.Image, d.SectorsFail)
}
// ParseZip reads a filanfc-dump zip and parses every sticker as Bambu.
func ParseZip(r io.Reader) ([]Tag, error) {
dumps, err := dump.ParseZip(r)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDump, err)
}
tags := make([]Tag, 0, len(dumps))
for _, d := range dumps {
tag, err := ParseDump(d)
if err != nil {
return nil, fmt.Errorf("chip %s: %w", d.ChipUID, err)
}
tags = append(tags, tag)
}
return tags, nil
}
+101
View File
@@ -0,0 +1,101 @@
package bambunfc
import (
"bytes"
"errors"
"os"
"testing"
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/dump"
)
func TestParseZipFixtures(t *testing.T) {
t.Parallel()
f, err := os.Open("testdata/dumps.zip")
if err != nil {
t.Fatal(err)
}
defer f.Close()
tags, err := ParseZip(f)
if err != nil {
t.Fatal(err)
}
if len(tags) != 6 {
t.Fatalf("len(tags) = %d; want 6", len(tags))
}
var translucent, basic int
for _, tag := range tags {
if tag.Brand != BrandBambuLab {
t.Errorf("chip %s brand %q", tag.ChipUID, tag.Brand)
}
if tag.DiameterMM != 1.75 {
t.Errorf("chip %s diameter %v", tag.ChipUID, tag.DiameterMM)
}
switch tag.Type {
case "PETG Translucent":
translucent++
if tag.Color.Hex() != "#61B0FF80" {
t.Errorf("translucent color %s", tag.Color.Hex())
}
case "PETG Basic":
basic++
if tag.Color.Hex() != "#000000FF" {
t.Errorf("basic color %s", tag.Color.Hex())
}
default:
t.Errorf("unexpected type %q", tag.Type)
}
}
if translucent != 4 || basic != 2 {
t.Errorf("translucent=%d basic=%d", translucent, basic)
}
}
func TestParseImageErrors(t *testing.T) {
t.Parallel()
raw, err := os.ReadFile("testdata/dumps/e27be276.bin")
if err != nil {
t.Fatal(err)
}
if _, err := ParseImage("e27be276", raw[:1023], nil); !errors.Is(err, ErrImageSize) {
t.Errorf("short image err = %v; want ErrImageSize", err)
}
if _, err := ParseImage("e27be276", raw, []int{2}); !errors.Is(err, ErrIncomplete) {
t.Errorf("fail sector 2 err = %v; want ErrIncomplete", err)
}
blank := make([]byte, 1024)
if _, err := ParseImage("e27be276", blank, nil); !errors.Is(err, ErrNotBambu) {
t.Errorf("blank image err = %v; want ErrNotBambu", err)
}
}
func TestParseDumpJSON(t *testing.T) {
t.Parallel()
f, err := os.Open("testdata/dumps/e27be276.json")
if err != nil {
t.Fatal(err)
}
defer f.Close()
d, err := dump.ParseJSON(f)
if err != nil {
t.Fatal(err)
}
tag, err := ParseDump(d)
if err != nil {
t.Fatal(err)
}
if tag.TrayUID != "2fb8e0e972084e74bf737393b28e7b12" {
t.Errorf("TrayUID = %s", tag.TrayUID)
}
if tag.ChipUID != "e27be276" {
t.Errorf("ChipUID = %s", tag.ChipUID)
}
}
func TestParseImageEmptyUID(t *testing.T) {
t.Parallel()
_, err := ParseImage("", bytes.Repeat([]byte{1}, 1024), nil)
if !errors.Is(err, ErrUID) {
t.Errorf("err = %v; want ErrUID", err)
}
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:17.612080Z","chip_uid":"52d60177","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"52d60177f2080400050f4bb2bb315e904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3e2fb8e0e972084e74bf737393b28e7b1200000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f323132303236303430320000000000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000156a277052e48d22d9c515522178c96700000000000087878769000000000000a092f47b7ddeee11ae5bd3209ea4eac7fcbc8c44b15f39c48031c63900c8c9fb970e215eb13b518763875c4f6de59c2b0000000000008787876900000000000084114ae56a384a765b1da4d0ea011fc768af8a9f5366b47fc5bfb15c3f4e97f350dfc8c51318e00eb9ca8f128a35667c000000000000878787690000000000008e579b88c536abca254e6161dfa3ecb373576204ab38589cc460d7018067bd43a89f15db64d8e5aed0a9839c458b6b3b000000000000878787690000000000003d5348fc8b55473868629b1e031d23cba4a682307f5a8a51c8416bac428b024e8775b8c071bc07e7e3df779e40b8eb5b00000000000087878769000000000000a07cc38936af652fb9c56e46129adf740d44b8fc902d588a367ef1efb5f88895bcd82eef67df0e2bbc215525a73c1d0000000000000087878769000000000000","dump_b64":"UtYBd/IIBAAFD0uyuzFekEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+L7jg6XIITnS/c3OTso57EgAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM18yMTIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFWoncFLkjSLZxRVSIXjJZwAAAAAAAIeHh2kAAAAAAACgkvR7fd7uEa5b0yCepOrH/LyMRLFfOcSAMcY5AMjJ+5cOIV6xO1GHY4dcT23lnCsAAAAAAACHh4dpAAAAAAAAhBFK5Wo4SnZbHaTQ6gEfx2ivip9TZrR/xb+xXD9Ol/NQ38jFExjgDrnKjxKKNWZ8AAAAAAAAh4eHaQAAAAAAAI5Xm4jFNqvKJU5hYd+j7LNzV2IEqzhYnMRg1wGAZ71DqJ8V22TY5a7QqYOcRYtrOwAAAAAAAIeHh2kAAAAAAAA9U0j8i1VHOGhimx4DHSPLpKaCMH9ailHIQWusQosCTod1uMBxvAfn4993nkC461sAAAAAAACHh4dpAAAAAAAAoHzDiTavZS+5xW5GEprfdA1EuPyQLViKNn7x77X4iJW82C7vZ98OK7whVSWnPB0AAAAAAAAAh4eHaQAAAAAAAA=="}
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:33.888563Z","chip_uid":"92a80577","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"92a805774808040005bbb1ffee6a26904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3eaa42f322172c48e99c0ece895346ea9f00000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f343632303236303430320000000000000000000000004a01000000000000000000000000000000008787876900000000000002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000001000000000000000000000000000000000000000000000000000000000000008d3fbd5a0eb9037c318d989139fd582600000000000087878769000000000000d22c7b4757adbcc88e3ebbc5ca1145711c97f643ebb67068e6b1bfee54057edc63f7c36e05159147152c62b270758d0700000000000087878769000000000000c42bbba5082f31656c9956313a49609810219e5bbbf372c822f1967919b663de6f20ead8ba298762d000440febc0634a000000000000878787690000000000005dc0beafe4dfeee033dcce4d5d9e232cb2aed198420f7bb2c06367b80d92210b9087789eae480234ad1f519f376ff17a00000000000087878769000000000000e4fcf2274514b9464159e8c1efc4423cfc42a0e1d2259ba287f62db47e559a405fef21496905c2db3eaedf84646d7d5e00000000000087878769000000000000319c5908b98ee01ffa1cc1713e2f9c3e0b98f6b35875aeb0c6feefa218b5a402abd20a09b7ebfbfbd0b6192cbf62c67500000000000087878769000000000000","dump_b64":"kqgFd0gIBAAFu7H/7momkEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+qkLzIhcsSOmcDs6JU0bqnwAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM180NjIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjT+9Wg65A3wxjZiROf1YJgAAAAAAAIeHh2kAAAAAAADSLHtHV628yI4+u8XKEUVxHJf2Q+u2cGjmsb/uVAV+3GP3w24FFZFHFSxisnB1jQcAAAAAAACHh4dpAAAAAAAAxCu7pQgvMWVsmVYxOklgmBAhnlu783LIIvGWeRm2Y95vIOrYuimHYtAARA/rwGNKAAAAAAAAh4eHaQAAAAAAAF3Avq/k3+7gM9zOTV2eIyyyrtGYQg97ssBjZ7gNkiELkId4nq5IAjStH1GfN2/xegAAAAAAAIeHh2kAAAAAAADk/PInRRS5RkFZ6MHvxEI8/EKg4dIlm6KH9i20flWaQF/vIUlpBcLbPq7fhGRtfV4AAAAAAACHh4dpAAAAAAAAMZxZCLmO4B/6HMFxPi+cPguY9rNYda6wxv7vohi1pAKr0goJt+v7+9C2GSy/YsZ1AAAAAAAAh4eHaQAAAAAAAA=="}
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:53:36.914443Z","chip_uid":"92b6fb31","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"92b6fb31ee0804000577a98edf9f09904730302d4b3030004746473030000000504554470000000000000000000000000000000000008787876900000000000050455447204261736963000000000000000000ffe80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000000000000000000000000000cdcc4c3e3cb568b7af4f41819412fe60afc5c44600000000ab0f0000000000000000000000000000000087878769000000000000323032365f30325f32355f31365f313932365f30325f32355f31360000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000475be12293a7f927bf42fa7b1aa7944a000000000000878787690000000000007f71f47a229a5dd5057f33aa2fee1531172829d75bf33a6c9f3e41365a1e844ff4f8528313f48fefaa1e6d02765e8abf0000000000008787876900000000000085b72ba08ddb600e8ce8d003758deca39e3ac4a83ef98429fc9d73a0ad596427aed0c23621aee0f7c25712a8e518e20c00000000000087878769000000000000e328f11df058429ddbd7ad67596e04800262e81492c59f53888bd32e65764727ad23acca7ed7a61f2c565595d39e4e79000000000000878787690000000000002f11ee1918f03618fd3c428911fb7b61cfed49fb7ebd25478eefe2e06fa633d2305950d0ced79ba6ede7a933ecaf81f500000000000087878769000000000000349b83bce314d86f4f02ddb897eb43e714dbec97891357686165cb66c230958a77e45cf5c1eb71d7aa9337499c59cbbc00000000000087878769000000000000","dump_b64":"krb7Me4IBAAFd6mO358JkEcwMC1LMDAAR0ZHMDAAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgQmFzaWMAAAAAAAAAAAD/6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAADNzEw+PLVot69PQYGUEv5gr8XERgAAAACrDwAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wMl8yNV8xNl8xOTI2XzAyXzI1XzE2AAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR1vhIpOn+Se/Qvp7GqeUSgAAAAAAAIeHh2kAAAAAAAB/cfR6Ippd1QV/M6ov7hUxFygp11vzOmyfPkE2Wh6ET/T4UoMT9I/vqh5tAnZeir8AAAAAAACHh4dpAAAAAAAAhbcroI3bYA6M6NADdY3so546xKg++YQp/J1zoK1ZZCeu0MI2Ia7g98JXEqjlGOIMAAAAAAAAh4eHaQAAAAAAAOMo8R3wWEKd29etZ1luBIACYugUksWfU4iL0y5ldkcnrSOsyn7Xph8sVlWV055OeQAAAAAAAIeHh2kAAAAAAAAvEe4ZGPA2GP08QokR+3thz+1J+369JUeO7+Lgb6Yz0jBZUNDO15um7eepM+yvgfUAAAAAAACHh4dpAAAAAAAANJuDvOMU2G9PAt24l+tD5xTb7JeJE1doYWXLZsIwlYp35Fz1wetx16qTN0mcWcu8AAAAAAAAh4eHaQAAAAAAAA=="}
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:52.668047Z","chip_uid":"b2d32177","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"b2d321773708040005116ac96f99fd904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3eaa42f322172c48e99c0ece895346ea9f00000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f343632303236303430320000000000000000000000004a01000000000000000000000000000000008787876900000000000002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000001000000000000000000000000000000000000000000000000000000000000001bc1b3d3eb58970ec6aa04d904b7280e00000000000087878769000000000000f7d96a1c22d18e419694cf375bc072f9f0325fdf07301faf4b7f63aec72f962863d4e42761e1d59c30292e45b8088bc800000000000087878769000000000000fadc47e3d52f8722df5e7613c9445c4f9a0a7a3a87362a4f8da04fdefbf082a98897d99124e9f61021e29c5e28c5ffd500000000000087878769000000000000fa03e9bc64418cee3eac18ac24a08e8195ebdb985b3780a570b83ac5c2b3e4576e80a5c5b4f5a44d80695cc9a1ea328700000000000087878769000000000000a56fb5e43f60cf4414a458576596307b2028bdd57e449e69cdccbdc5cfb5b119ebc6f1788549fdfbfcf0478c0f9d386700000000000087878769000000000000416768587629189df719eb6b91851f6191241892547475cddc0ce27eb6ba4f5e8bba9736223405f48296d27b5a5b82b500000000000087878769000000000000","dump_b64":"stMhdzcIBAAFEWrJb5n9kEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+qkLzIhcsSOmcDs6JU0bqnwAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM180NjIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG8Gz0+tYlw7GqgTZBLcoDgAAAAAAAIeHh2kAAAAAAAD32WocItGOQZaUzzdbwHL58DJf3wcwH69Lf2Ouxy+WKGPU5Cdh4dWcMCkuRbgIi8gAAAAAAACHh4dpAAAAAAAA+txH49UvhyLfXnYTyURcT5oKejqHNipPjaBP3vvwgqmIl9mRJOn2ECHinF4oxf/VAAAAAAAAh4eHaQAAAAAAAPoD6bxkQYzuPqwYrCSgjoGV69uYWzeApXC4OsXCs+RXboClxbT1pE2AaVzJoeoyhwAAAAAAAIeHh2kAAAAAAAClb7XkP2DPRBSkWFdlljB7ICi91X5EnmnNzL3Fz7WxGevG8XiFSf37/PBHjA+dOGcAAAAAAACHh4dpAAAAAAAAQWdoWHYpGJ33GetrkYUfYZEkGJJUdHXN3Azifra6T16Lupc2IjQF9IKW0ntaW4K1AAAAAAAAh4eHaQAAAAAAAA=="}
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:02.388908Z","chip_uid":"e27be276","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"e27be2760d08040005919d6a17ddcf904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3e2fb8e0e972084e74bf737393b28e7b1200000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f323132303236303430320000000000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000630fe06e572f8f0f94e2ff3bfba403190000000000008787876900000000000074eaa3ac7501ae371180023e3e834388526896deb1450667081e07af5ad55257dabb43981c79bd7263072f7d4727292f000000000000878787690000000000004f52ee7faa3adf093b19814efac2123f66a0cd95e9da3abc88cb652f284bc98e6d1098712a24e49ae0d4c682783cb6b400000000000087878769000000000000433a59c30c8920059b3d004360d17bc2670314a1a927acf0527258297d89bd72301337347a597fd7135d9af0a436e437000000000000878787690000000000006fd6cc3b82cd5c326eb0ff87104db3440322b1e578cf30410f2fc66271da62bd627347fea583a136f72599a900ccf14e000000000000878787690000000000002e07c6b457d7672aa10ed15b889180dedeb1a26fceb509fe96da5740c5c234dae1a96fe45a819a4acfbdaa225931999800000000000087878769000000000000","dump_b64":"4nvidg0IBAAFkZ1qF93PkEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+L7jg6XIITnS/c3OTso57EgAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM18yMTIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYw/gblcvjw+U4v87+6QDGQAAAAAAAIeHh2kAAAAAAAB06qOsdQGuNxGAAj4+g0OIUmiW3rFFBmcIHgevWtVSV9q7Q5gceb1yYwcvfUcnKS8AAAAAAACHh4dpAAAAAAAAT1Luf6o63wk7GYFO+sISP2agzZXp2jq8iMtlLyhLyY5tEJhxKiTkmuDUxoJ4PLa0AAAAAAAAh4eHaQAAAAAAAEM6WcMMiSAFmz0AQ2DRe8JnAxShqSes8FJyWCl9ib1yMBM3NHpZf9cTXZrwpDbkNwAAAAAAAIeHh2kAAAAAAABv1sw7gs1cMm6w/4cQTbNEAyKx5XjPMEEPL8ZicdpivWJzR/6lg6E29yWZqQDM8U4AAAAAAACHh4dpAAAAAAAALgfGtFfXZyqhDtFbiJGA3t6xom/OtQn+ltpXQMXCNNrhqW/kWoGaSs+9qiJZMZmYAAAAAAAAh4eHaQAAAAAAAA=="}
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:53:49.531896Z","chip_uid":"f28c58ed","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"f28c58edcb08040005fbe0f72ba5ad904730302d4b3030004746473030000000504554470000000000000000000000000000000000008787876900000000000050455447204261736963000000000000000000ffe80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000000000000000000000000000cdcc4c3e3cb568b7af4f41819412fe60afc5c44600000000ab0f0000000000000000000000000000000087878769000000000000323032365f30325f32355f31365f313932365f30325f32355f31360000000000000000004a010000000000000000000000000000000087878769000000000000020001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000010000000000000000000000000000000000000000000000000000000000000050ee165b9ac59477618c747996394944000000000000878787690000000000000821e5296e38edaf4e579c0a3e385a718d7cd7cae8bcf1885ab51da9d642d7b5d00bd7bf9222d1cdd573728a842d9328000000000000878787690000000000004b929502ea8bdcc645a7d415cfeb6c0449095bf56227d777936a3492e256583dbd494e976e67c421c7293696fea9380700000000000087878769000000000000b96ad7d6fcd727be230357d9e228a669819e5727dfb287e0649347da066ca2e1e6b043aea72f43b7aae856d3fd1cc30b00000000000087878769000000000000be81eea3831737d3e3fdeb81e0b360206d3db6f50864effbf3e784a8547f0a9a730e3af09b052b0de6f7fe87959cee4f00000000000087878769000000000000e3f7d07cc16a75a374b81b53a73256b34921a3fcf6e6688a9925c40e2acb6786bf4457280976268f5703988b49b73a3400000000000087878769000000000000","dump_b64":"8oxY7csIBAAF++D3K6WtkEcwMC1LMDAAR0ZHMDAAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgQmFzaWMAAAAAAAAAAAD/6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAADNzEw+PLVot69PQYGUEv5gr8XERgAAAACrDwAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wMl8yNV8xNl8xOTI2XzAyXzI1XzE2AAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUO4WW5rFlHdhjHR5ljlJRAAAAAAAAIeHh2kAAAAAAAAIIeUpbjjtr05XnAo+OFpxjXzXyui88YhatR2p1kLXtdAL17+SItHN1XNyioQtkygAAAAAAACHh4dpAAAAAAAAS5KVAuqL3MZFp9QVz+tsBEkJW/ViJ9d3k2o0kuJWWD29SU6XbmfEIccpNpb+qTgHAAAAAAAAh4eHaQAAAAAAALlq19b81ye+IwNX2eIopmmBnlcn37KH4GSTR9oGbKLh5rBDrqcvQ7eq6FbT/RzDCwAAAAAAAIeHh2kAAAAAAAC+ge6jgxc30+P964Hgs2AgbT229Qhk7/vz54SoVH8KmnMOOvCbBSsN5vf+h5Wc7k8AAAAAAACHh4dpAAAAAAAA4/fQfMFqdaN0uBtTpzJWs0kho/z25miKmSXEDirLZ4a/RFcoCXYmj1cDmItJtzo0AAAAAAAAh4eHaQAAAAAAAA=="}