Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/tle/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ Example:
$ tle -D 10d -o encrypted_file data_to_encrypt

After the specified duration:
$ tle -d -o decrypted_file.txt encrypted_file`
$ tle -d -o decrypted_file.txt encrypted_file

Metadata examples:
$ tle -m # Prints network metadata (YAML)
$ tle -m encrypted.age # Prints ciphertext metadata (round, chain, time)`

// PrintUsage displays the usage information.
func PrintUsage(log *log.Logger) {
Expand Down
136 changes: 136 additions & 0 deletions cmd/tle/commands/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package commands

import (
"bufio"
"encoding/base64"
"fmt"
"io"
"strconv"
"strings"
"time"

"filippo.io/age/armor"
"gopkg.in/yaml.v3"

"github.com/drand/tlock/networks/http"
)

type CiphertextMetadata struct {
Round uint64 `yaml:"round"`
ChainHash string `yaml:"chain_hash"`
Time time.Time `yaml:"time"`
}

// Metadata reads INPUT from src and, if it contains a tlock stanza, outputs YAML with round, chainhash and estimated time.
func Metadata(dst io.Writer, src io.Reader, network *http.Network) error {
Comment thread
alienx5499 marked this conversation as resolved.
rr := bufio.NewReader(src)
// Only support armored input for metadata extraction in this change.
if start, _ := rr.Peek(len(armor.Header)); string(start) != armor.Header {
return fmt.Errorf("metadata from INPUT currently supports only armored age files")
}
Comment thread
alienx5499 marked this conversation as resolved.
Outdated
// Read base64-encoded header block: try decoding each line until we find the tlock stanza.
if _, err := rr.ReadString('\n'); err != nil { // BEGIN line
return fmt.Errorf("read begin line: %w", err)
}
var round uint64
var chainHash string
found := false
for {
line, err := rr.ReadString('\n')
if err != nil {
return fmt.Errorf("read armored content: %w", err)
}
s := strings.TrimSpace(line)
if s == "" {
continue
}
if strings.HasPrefix(s, "-----END ") {
break
}
// Try to decode this line as base64
dec, err := base64.StdEncoding.DecodeString(s)
if err != nil {
continue
}
decoded := string(dec)
// Look for tlock stanza in this decoded line
for _, line := range strings.Split(decoded, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "-> ") {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[1] == "tlock" {
r, err := strconv.ParseUint(fields[2], 10, 64)
if err != nil {
return fmt.Errorf("parse round: %w", err)
}
round = r
chainHash = fields[3]
found = true
break
}
}
}
if found {
break
}
}
if !found {
return fmt.Errorf("no tlock stanza found in armored age header")
}

// Estimate time for the given round
now := time.Now()
current := network.Current(now)
var low, high time.Time
if round <= current {
high = now
low = now.Add(-365 * 24 * time.Hour)
} else {
low = now
high = now.Add(365 * 24 * time.Hour)
}

t, err := roundToTimeBinarySearch(network, round, low, high)
if err != nil {
return fmt.Errorf("estimate time: %w", err)
}

out := CiphertextMetadata{Round: round, ChainHash: chainHash, Time: t}
b, err := yaml.Marshal(out)
if err != nil {
return fmt.Errorf("yaml marshal: %w", err)
}
if _, err := dst.Write(b); err != nil {
return fmt.Errorf("write: %w", err)
}
return nil
}

// roundToTimeBinarySearch searches for a time whose round is the target.
func roundToTimeBinarySearch(network *http.Network, target uint64, low, high time.Time) (time.Time, error) {
// If bounds are inverted, fix.
if high.Before(low) {
low, high = high, low
}
// Binary search with tolerance of 1 round.
for i := 0; i < 64; i++ {
mid := low.Add(high.Sub(low) / 2)
r := network.RoundNumber(mid)
if r == target {
return mid, nil
}
if r < target {
low = mid.Add(time.Second)
} else {
high = mid.Add(-time.Second)
}
if !high.After(low) {
break
}
}
// Best effort: return low as approximation.
return low, nil
}

// parseArgs tries to extract round and chain from a tlock stanza arguments slice.
// (no other helpers)
6 changes: 5 additions & 1 deletion cmd/tle/tle.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ func run() error {

switch {
case flags.Metadata:
err = tlock.New(network).Metadata(dst)
if name := flag.Arg(0); name != "" && name != "-" {
err = commands.Metadata(dst, src, network)
} else {
err = tlock.New(network).Metadata(dst)
}
case flags.Decrypt:
err = tlock.New(network).Decrypt(dst, src)
default:
Expand Down