Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 44 additions & 0 deletions cmd/tle/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package commands
import (
"bytes"
"os"
"path/filepath"
"testing"
"time"

"github.com/drand/tlock/networks/http"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestParseDuration(t *testing.T) {
Expand Down Expand Up @@ -148,3 +151,44 @@ func TestEncryptionWithDurationOverflowUsingOtherUnits(t *testing.T) {
err := Encrypt(flags, os.Stdout, bytes.NewBufferString("very nice"), nil)
require.ErrorIs(t, err, ErrInvalidDurationValue)
}

func TestMetadata(t *testing.T) {
if testing.Short() {
t.Skip("skipping network test in short mode")
}

// use testnet quicknet-t network matching the testdata file
testnetHost := "http://pl-eu.testnet.drand.sh"
testnetQuicknetT := "cc9c398442737cbd141526600919edd69f1d6f9b4adb67e4d912fbc64341a9a5"

network, err := http.NewNetwork(testnetHost, testnetQuicknetT)
require.NoError(t, err)

// open the testdata file
testdataPath := filepath.Join("..", "..", "..", "testdata", "lorem-tle-testnet-quicknet-t-2024-01-17-15-28.tle")
f, err := os.Open(testdataPath)
require.NoError(t, err)
defer f.Close()

// extract metadata
var output bytes.Buffer
err = Metadata(&output, f, network)
require.NoError(t, err)

// verify output is valid YAML with expected fields
var metadata CiphertextMetadata
err = yaml.Unmarshal(output.Bytes(), &metadata)
require.NoError(t, err)

// verify fields are populated
require.NotZero(t, metadata.Round, "round should be non-zero")
require.Equal(t, testnetQuicknetT, metadata.ChainHash, "chain hash should match")
require.False(t, metadata.Time.IsZero(), "time should be set")

// verify the output contains the expected YAML keys
outputStr := output.String()
require.Contains(t, outputStr, "round:", "output should contain round field")
require.Contains(t, outputStr, "chain_hash:", "output should contain chain_hash field")
require.Contains(t, outputStr, "time:", "output should contain time field")
require.Contains(t, outputStr, testnetQuicknetT, "output should contain the chain hash value")
}
117 changes: 117 additions & 0 deletions cmd/tle/commands/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package commands

import (
"bufio"
"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)

// Use armor.NewReader to handle armor decoding automatically
// Only support armored input for metadata extraction in this change.
armorReader := armor.NewReader(rr)

// Read from the de-armored content to find the tlock stanza
scanner := bufio.NewScanner(armorReader)
var round uint64
var chainHash string
found := false

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
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 err := scanner.Err(); err != nil {
return fmt.Errorf("read armored content: %w", err)
}

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