mirror of
https://github.com/FiloSottile/age.git
synced 2026-02-06 18:20:42 +00:00
128 lines
3.4 KiB
Go
128 lines
3.4 KiB
Go
package inspect
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"filippo.io/age/armor"
|
|
"filippo.io/age/internal/format"
|
|
"filippo.io/age/internal/stream"
|
|
)
|
|
|
|
type Metadata struct {
|
|
Version string `json:"version"`
|
|
Postquantum string `json:"postquantum"` // "yes" or "no" or "unknown"
|
|
Armor bool `json:"armor"`
|
|
StanzaTypes []string `json:"stanza_types"`
|
|
Sizes struct {
|
|
Header int64 `json:"header"`
|
|
Armor int64 `json:"armor"`
|
|
Overhead int64 `json:"overhead"`
|
|
// Currently, we don't do any padding, not MinPayload == MaxPayload and
|
|
// MinPadding == MaxPadding == 0, but that might change in the future.
|
|
MinPayload int64 `json:"min_payload"`
|
|
MaxPayload int64 `json:"max_payload"`
|
|
MinPadding int64 `json:"min_padding"`
|
|
MaxPadding int64 `json:"max_padding"`
|
|
} `json:"sizes"`
|
|
}
|
|
|
|
func Inspect(r io.Reader, fileSize int64) (*Metadata, error) {
|
|
data := &Metadata{
|
|
Version: "age-encryption.org/v1",
|
|
Postquantum: "unknown",
|
|
}
|
|
|
|
tr := &trackReader{r: r}
|
|
br := bufio.NewReader(tr)
|
|
const maxWhitespace = 1024
|
|
start, _ := br.Peek(maxWhitespace + len(armor.Header))
|
|
if strings.HasPrefix(string(bytes.TrimSpace(start)), armor.Header) {
|
|
r = armor.NewReader(br)
|
|
data.Armor = true
|
|
} else {
|
|
r = br
|
|
}
|
|
|
|
hdr, rest, err := format.Parse(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read header: %w", err)
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
if err := hdr.Marshal(buf); err != nil {
|
|
return nil, fmt.Errorf("failed to re-serialize header: %w", err)
|
|
}
|
|
data.Sizes.Header = int64(buf.Len())
|
|
|
|
for _, s := range hdr.Recipients {
|
|
data.StanzaTypes = append(data.StanzaTypes, s.Type)
|
|
switch s.Type {
|
|
case "X25519", "ssh-rsa", "ssh-ed25519", "age-encryption.org/p256tag", "piv-p256":
|
|
data.Postquantum = "no"
|
|
case "mlkem768x25519", "scrypt", "age-encryption.org/mlkem768p256tag":
|
|
if data.Postquantum != "no" {
|
|
data.Postquantum = "yes"
|
|
}
|
|
}
|
|
}
|
|
|
|
// If fileSize is not provided, or if it's the size of the armored file
|
|
// (which can have LF or CRLF line endings, varying its size), read to
|
|
// the end to determine it.
|
|
if fileSize == -1 || data.Armor {
|
|
n, err := io.Copy(io.Discard, rest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read rest of file: %w", err)
|
|
}
|
|
fileSize = data.Sizes.Header + n
|
|
if !tr.done {
|
|
panic("trackReader not done after io.Copy")
|
|
}
|
|
if tr.count != fileSize && !data.Armor {
|
|
panic("trackReader count mismatch")
|
|
}
|
|
data.Sizes.Armor = tr.count - fileSize
|
|
}
|
|
data.Sizes.Overhead, err = streamOverhead(fileSize - data.Sizes.Header)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compute stream overhead: %w", err)
|
|
}
|
|
data.Sizes.MinPayload = fileSize - data.Sizes.Header - data.Sizes.Overhead
|
|
data.Sizes.MaxPayload = data.Sizes.MinPayload
|
|
return data, nil
|
|
}
|
|
|
|
type trackReader struct {
|
|
r io.Reader
|
|
count int64
|
|
done bool
|
|
}
|
|
|
|
func (tr *trackReader) Read(p []byte) (int, error) {
|
|
n, err := tr.r.Read(p)
|
|
tr.count += int64(n)
|
|
if err == io.EOF {
|
|
tr.done = true
|
|
} else if tr.done {
|
|
panic("non-EOF read after EOF")
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func streamOverhead(payloadSize int64) (int64, error) {
|
|
const streamNonceSize = 16
|
|
if payloadSize < streamNonceSize {
|
|
return 0, fmt.Errorf("encrypted size too small: %d", payloadSize)
|
|
}
|
|
encryptedSize := payloadSize - streamNonceSize
|
|
plaintextSize, err := stream.PlaintextSize(encryptedSize)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return payloadSize - plaintextSize, nil
|
|
}
|