package archive import ( "archive/tar" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "log" "os" "os/exec" "path/filepath" "time" "kisekinopureya.com.tr/updater/internal/version" ) // Output metadata structure type Output struct { Metadata struct { Date int64 `json:"date"` Version string `json:"version"` } `json:"metadata"` Files []struct { Name string `json:"name"` Type string `json:"type"` Sha256 string `json:"sha256"` } `json:"files"` } type fileInfo struct { Name string Type string Path string Sha256 string } // Pack creates a tar.xz archive with metadata.json and the given images func Pack(rootfsPath, verityPath, kernelPath, outPath string) error { paths := []struct { Path string Type string }{ {rootfsPath, "rootfs"}, {verityPath, "dm-verity"}, {kernelPath, "kernel"}, } var infos []fileInfo for _, p := range paths { log.Printf("Hashing %s...", p.Path) sum, err := sha256File(p.Path) if err != nil { return fmt.Errorf("hashing %s: %w", p.Path, err) } infos = append(infos, fileInfo{ Name: filepath.Base(p.Path), Type: p.Type, Path: p.Path, Sha256: sum, }) } output, err := exec.Command("blkid", rootfsPath).Output() if err != nil { log.Fatal(err) } versionFromImage, err := version.Parse(string(output)) if err != nil { log.Fatal(err) } var out Output out.Metadata.Date = time.Now().Unix() // NOTE: version should be filled by caller if needed out.Metadata.Version = versionFromImage for _, fi := range infos { out.Files = append(out.Files, struct { Name string `json:"name"` Type string `json:"type"` Sha256 string `json:"sha256"` }{ Name: fi.Name, Type: fi.Type, Sha256: fi.Sha256, }) } metaBytes, err := json.MarshalIndent(out, "", " ") if err != nil { return fmt.Errorf("marshal metadata: %w", err) } log.Printf("Metadata: %+v\n", out) if err := writeTarXz(outPath, metaBytes, infos); err != nil { return fmt.Errorf("create tar.xz: %w", err) } sigFile := outPath + ".sig" cmd := exec.Command("gpg", "--output", sigFile, "--detach-sign", "--armor", outPath) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { panic(fmt.Errorf("gpg signing failed: %w", err)) } fmt.Printf("Created %s and signed as %s\n", outPath, sigFile) return nil } func sha256File(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } func writeTarXz(outPath string, metadata []byte, files []fileInfo) error { outFile, err := os.Create(outPath) if err != nil { return fmt.Errorf("create output: %w", err) } defer outFile.Close() cmd := exec.Command("xz", "-z", "-c") cmd.Stdout = outFile stdin, err := cmd.StdinPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } tw := tar.NewWriter(stdin) // metadata.json metaHeader := &tar.Header{ Name: "metadata.json", Mode: 0644, Size: int64(len(metadata)), } if err := tw.WriteHeader(metaHeader); err != nil { return err } if _, err := tw.Write(metadata); err != nil { return err } // files for _, fi := range files { if err := addFileToTar(tw, fi.Path, fi.Name); err != nil { return err } } if err := tw.Close(); err != nil { return err } if err := stdin.Close(); err != nil { return err } if err := cmd.Wait(); err != nil { return err } return nil } func addFileToTar(tw *tar.Writer, srcPath, destName string) error { f, err := os.Open(srcPath) if err != nil { return err } defer f.Close() stat, err := f.Stat() if err != nil { return err } hdr := &tar.Header{ Name: destName, Mode: int64(stat.Mode().Perm()), Size: stat.Size(), ModTime: stat.ModTime(), } if err := tw.WriteHeader(hdr); err != nil { return err } _, err = io.Copy(tw, f) return err }