Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
package capsule_test
import (
"bytes"
"encoding/hex"
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
"errors"
"io"
"reflect"
"strings"
"testing"
"time"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/accesskey"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/wordkey"
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
)
// The tests of this file cover the writer of format 3 (spec v0.10, §61,
// §62, §62.1).
// files3 returns the options of a capsule of round 1000, written at the
// Quicknet genesis.
func files3 ( t * testing . T ) capsule . EncryptOptions {
opts := past ( t , 1000 )
opts . TestVectors = false
return opts
}
// Spec §29.2 to §29.6, §61: what EncryptFiles writes, Open reads back, with
// the files in the byte order of their paths (R8), the comment with LF, the
// mtime only within its range, and a fresh salt.
func TestEncryptFilesRoundTrip ( t * testing . T ) {
photo := strings . Repeat ( "playa" , 30000 ) // three STREAM chunks
when := time . Date ( 2026 , 9 , 30 , 18 , 0 , 0 , 0 , time . UTC )
sources := [ ] capsule . Source {
source ( "vacío.txt" , "" ) ,
source ( "nota.txt" , "Hola.\n" ) ,
source ( "fotos/playa.jpg" , photo ) ,
source ( "\U0001F600.txt" , "emoji" ) ,
source ( "\uFF5E.txt" , "tilde" ) , // before U+1F600 in UTF-8, after it in UTF-16
source ( "fotos/a.txt" , "a" ) ,
}
sources [ 1 ] . ModTime = when
sources [ 2 ] . ModTime = time . Date ( 10000 , 1 , 1 , 0 , 0 , 0 , 0 , time . UTC ) // after 9999: omitted
sources [ 3 ] . ModTime = time . Date ( 1969 , 12 , 31 , 0 , 0 , 0 , 0 , time . UTC ) // before 1970: omitted
note , err := extension . New ( "org.example.note" , 1 , [ ] byte ( "x" ) )
if err != nil {
t . Fatal ( err )
}
opts := files3 ( t )
opts . Comment , opts . Author = "Hola\r\nmundo\rfin" , "Ana López"
opts . HeadNoncritical = [ ] extension . Extension { note }
var dkc bytes . Buffer
res , err := capsule . EncryptFiles ( & dkc , sources , opts )
if err != nil {
t . Fatal ( err )
}
var paths [ ] string
for _ , f := range res . Head . Files {
paths = append ( paths , f . Path )
}
want := [ ] string { "fotos/a.txt" , "fotos/playa.jpg" , "nota.txt" , "vacío.txt" , "\uFF5E.txt" , "\U0001F600.txt" }
switch {
case res . Format != capsule . Format3 || res . Padding != capsule . Reforzado :
t . Errorf ( "format %d, padding %s" , res . Format , res . Padding )
case ! reflect . DeepEqual ( paths , want ) :
t . Errorf ( "paths %q, want %q" , paths , want )
case res . Head . Comment != "Hola\nmundo\nfin" || res . Head . Author != opts . Author :
t . Errorf ( "comment %q, author %q" , res . Head . Comment , res . Head . Author )
case ! res . Head . Files [ 2 ] . HasMTime || res . Head . Files [ 2 ] . MTime != uint64 ( when . Unix ( ) ) :
t . Errorf ( "mtime of nota.txt: %v %d" , res . Head . Files [ 2 ] . HasMTime , res . Head . Files [ 2 ] . MTime )
case res . Head . Files [ 1 ] . HasMTime || res . Head . Files [ 5 ] . HasMTime || res . Head . Files [ 0 ] . HasMTime :
t . Error ( "an mtime out of range, or unknown, was written" )
case res . Head . Salt == [ capsule . SaltSize ] byte { } :
t . Error ( "zero salt" )
}
r := open3 ( t , dkc . Bytes ( ) , & testkit . MemorySink { } )
if r . err != nil {
t . Fatal ( r . err )
}
o := r . opened
if ! reflect . DeepEqual ( o . Head , res . Head ) {
t . Errorf ( "head read %+v, written %+v" , o . Head , res . Head )
}
if o . PayloadLength != res . Length || o . PaddedLength != res . PaddedLength || o . AreaLen != capsule . AreaLen {
t . Errorf ( "L = %d, P = %d, area %d; written L = %d, P = %d" , o . PayloadLength , o . PaddedLength , o . AreaLen , res . Length , res . PaddedLength )
}
if o . Verdicts != ( capsule . Verdicts { Signature : capsule . VerdictNoSignature , Seal : capsule . VerdictNoSeal } ) {
t . Errorf ( "verdicts %+v" , o . Verdicts )
}
byPath := map [ string ] string { }
for _ , s := range sources {
rc , _ := s . Open ( )
b , _ := io . ReadAll ( rc )
byPath [ s . Path ] = string ( b )
}
for i , f := range o . Head . Files {
if string ( r . sink . Files [ i ] ) != byPath [ f . Path ] {
t . Errorf ( "%s: %d bytes" , f . Path , len ( r . sink . Files [ i ] ) )
}
}
// The same files in another order give the same head, but for the salt.
var again bytes . Buffer
res2 , err := capsule . EncryptFiles ( & again , [ ] capsule . Source { sources [ 5 ] , sources [ 4 ] , sources [ 3 ] , sources [ 2 ] , sources [ 1 ] , sources [ 0 ] } , opts )
if err != nil {
t . Fatal ( err )
}
if res2 . Head . Salt == res . Head . Salt {
t . Error ( "the salt was reused" )
}
res2 . Head . Salt = res . Head . Salt
if ! reflect . DeepEqual ( res2 . Head , res . Head ) {
t . Error ( "the order of the sources changed the head" )
}
}
// Spec §29.2: the lengths of its examples, written by EncryptFiles, with the
// area of 32 KiB of v0.11: 32256 bytes more than with that of 512 of v0.10.
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
func TestEncryptFilesLengths ( t * testing . T ) {
note := source ( "nota.txt" , strings . Repeat ( "n" , 1000 ) )
note . ModTime = time . Date ( 2026 , 9 , 30 , 0 , 0 , 0 , 0 , time . UTC )
for _ , tc := range [ ] struct {
name string
sources [ ] capsule . Source
comment string
l , p uint64
} {
{ "a comment of one byte" , nil , "a" , 32836 , 34816 } ,
{ "nota.txt of 1000 bytes, with mtime" , [ ] capsule . Source { note } , "" , 33897 , 34816 } ,
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
} {
opts := files3 ( t )
opts . Comment = tc . comment
res , err := capsule . EncryptFiles ( io . Discard , tc . sources , opts )
if err != nil || res . Length != tc . l || res . PaddedLength != tc . p {
t . Errorf ( "%s: L = %d, P = %d, %v; want %d, %d" , tc . name , res . Length , res . PaddedLength , err , tc . l , tc . p )
}
}
}
// Spec §62, §38: time_and_key, with a portable key and a recipient.
func TestEncryptFilesTimeAndKey ( t * testing . T ) {
holder , _ := age . GenerateX25519Identity ( )
opts := files3 ( t )
opts . Policy , opts . NewPortableKey , opts . Recipients = capsule . TimeAndKey , true , [ ] age . Recipient { holder . Recipient ( ) }
var dkc bytes . Buffer
res , err := capsule . EncryptFiles ( & dkc , [ ] capsule . Source { source ( "a.txt" , "secreto" ) } , opts )
if err != nil {
t . Fatal ( err )
}
if r := open3 ( t , dkc . Bytes ( ) , & testkit . MemorySink { } , holder ) ; r . err != nil || string ( r . sink . Files [ 0 ] ) != "secreto" {
t . Errorf ( "with the identity: %v" , r . err )
}
var dkk bytes . Buffer
if err := accesskey . Encode ( & dkk , res . PortableKey ) ; err != nil {
t . Fatal ( err )
}
key , err := accesskey . Decode ( & dkk )
if err != nil {
t . Fatal ( err )
}
o := defaultOpen ( 1000 )
o . AccessKey , o . Sink = key , & testkit . MemorySink { }
if _ , err := capsule . Open ( t . Context ( ) , nil , bytes . NewReader ( dkc . Bytes ( ) ) , o ) ; err != nil {
t . Errorf ( "with the .dkk: %v" , err )
}
}
// Spec §38.1: a key of words is salted with the capsule_id that EncryptFiles
// draws, and its identity opens the capsule.
func TestEncryptFilesWords ( t * testing . T ) {
opts := files3 ( t )
words := wordkey . Normalize ( "Perro luna casa verde trén mar" )
opts . Policy , opts . Words = capsule . TimeAndKey , words
var dkc bytes . Buffer
res , err := capsule . EncryptFiles ( & dkc , [ ] capsule . Source { source ( "a.txt" , "secreto" ) } , opts )
if err != nil {
t . Fatal ( err )
}
chain , _ := hex . DecodeString ( profile . QuicknetChainHash )
id , err := wordkey . Identity ( words , chain , 1000 , res . CapsuleID [ : ] )
if err != nil {
t . Fatal ( err )
}
if r := open3 ( t , dkc . Bytes ( ) , & testkit . MemorySink { } , id ) ; r . err != nil || string ( r . sink . Files [ 0 ] ) != "secreto" {
t . Errorf ( "with the words: %v" , r . err )
}
other := res . CapsuleID
other [ 0 ] ^ = 1
if wrong , err := wordkey . Identity ( words , chain , 1000 , other [ : ] ) ; err != nil {
t . Fatal ( err )
} else if r := open3 ( t , dkc . Bytes ( ) , & testkit . MemorySink { } , wrong ) ; r . err == nil {
t . Error ( "the words of another capsule_id opened it" )
}
}
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// Spec §62.1 rules 3, 14, 15 and 18: EncryptFiles rejects what the reader
// would reject, and a file whose size is not its Size, before it writes
// anything, with a message that names the rule and the character.
func TestEncryptFilesRejects ( t * testing . T ) {
failing := source ( "a.txt" , "a" )
failing . Open = func ( ) ( io . ReadCloser , error ) { return nil , errors . New ( "permission denied" ) }
negative := source ( "a.txt" , "" )
negative . Size = - 1
nilOpen := source ( "a.txt" , "a" )
nilOpen . Open = nil
short , long := source ( "a.txt" , "abc" ) , source ( "a.txt" , "abc" )
short . Size , long . Size = 4 , 2
for _ , tc := range [ ] struct {
name string
sources [ ] capsule . Source
edit func ( o * capsule . EncryptOptions )
want string
} {
{ "nothing" , nil , nil , "at least one file or a comment" } ,
{ "an author alone" , nil , func ( o * capsule . EncryptOptions ) { o . Author = "Ana" } , "at least one file or a comment" } ,
{ "Length" , [ ] capsule . Source { source ( "a" , "a" ) } , func ( o * capsule . EncryptOptions ) { o . Length = 1 } , "Length is for Encrypt" } ,
{ "TAB in a path" , [ ] capsule . Source { source ( "a\tb" , "" ) } , nil , ` path "a\tb": R4: segment 1: control U+0009 ` } ,
{ "empty path" , [ ] capsule . Source { source ( "" , "" ) } , nil , ` path "": R1: 0 bytes ` } ,
{ "path of 1025 bytes" , [ ] capsule . Source { source ( strings . Repeat ( "a/" , 512 ) + "a" , "" ) } , nil , "R1: 1025 bytes" } ,
{ "path not UTF-8" , [ ] capsule . Source { source ( "a\xffb" , "" ) } , nil , "R1: not valid UTF-8" } ,
{ "path .." , [ ] capsule . Source { source ( ".." , "" ) } , nil , ` path "..": R3 ` } ,
{ "path with U+202E" , [ ] capsule . Source { source ( "a\u202eb" , "" ) } , nil , "R4: segment 1: invisible U+202E" } ,
{ "CON.txt" , [ ] capsule . Source { source ( "CON.txt" , "" ) } , nil , "R6" } ,
{ "a path twice" , [ ] capsule . Source { source ( "a.txt" , "" ) , source ( "a.txt" , "" ) } , nil , ` path "a.txt" given twice ` } ,
{ "A.txt and a.txt" , [ ] capsule . Source { source ( "a.txt" , "" ) , source ( "A.txt" , "" ) } , nil , ` paths "A.txt" and "a.txt": R7 ` } ,
{ "a and a/b" , [ ] capsule . Source { source ( "a/b" , "" ) , source ( "a" , "" ) } , nil , ` paths "a" and "a/b": R7 ` } ,
{ "comment with U+202E" , [ ] capsule . Source { source ( "a" , "" ) } , func ( o * capsule . EncryptOptions ) { o . Comment = "a\u202eb" } , "comment: text: bidirectional control U+202E" } ,
{ "comment of 16385 bytes" , nil , func ( o * capsule . EncryptOptions ) { o . Comment = strings . Repeat ( "a" , 16385 ) } , "comment: 16385 bytes, more than 16384" } ,
{ "comment not UTF-8" , nil , func ( o * capsule . EncryptOptions ) { o . Comment = "a\xff" } , "comment: not valid UTF-8" } ,
{ "author with LF" , nil , func ( o * capsule . EncryptOptions ) { o . Comment , o . Author = "c" , "Ana\nLópez" } , "declared author: text: control U+000A" } ,
{ "author with a leading space" , nil , func ( o * capsule . EncryptOptions ) { o . Comment , o . Author = "c" , " Ana" } , "starts or ends with U+0020" } ,
{ "author of 257 bytes" , nil , func ( o * capsule . EncryptOptions ) { o . Comment , o . Author = "c" , strings . Repeat ( "a" , 257 ) } , "more than 256" } ,
{ "negative size" , [ ] capsule . Source { negative } , nil , "negative size" } ,
{ "nil Open" , [ ] capsule . Source { nilOpen } , nil , "Source.Open is nil" } ,
{ "Open fails" , [ ] capsule . Source { failing } , nil , "permission denied" } ,
{ "shorter than its size" , [ ] capsule . Source { short } , nil , ` file "a.txt": 3 bytes, not its size of 4 ` } ,
{ "longer than its size" , [ ] capsule . Source { long } , nil , ` file "a.txt": more than its size of 2 bytes ` } ,
{ "65536 files" , make ( [ ] capsule . Source , 65536 ) , nil , "65536 files, more than 65535" } ,
{ "time_only with a recipient" , [ ] capsule . Source { source ( "a" , "" ) } , func ( o * capsule . EncryptOptions ) {
id , _ := age . GenerateX25519Identity ( )
o . Recipients = [ ] age . Recipient { id . Recipient ( ) }
} , "time_only takes no recipients" } ,
{ "time_only with words" , [ ] capsule . Source { source ( "a" , "" ) } , func ( o * capsule . EncryptOptions ) {
o . Words = wordkey . Normalize ( "perro luna casa verde tren mar" )
} , "no key of words" } ,
{ "too few words" , [ ] capsule . Source { source ( "a" , "" ) } , func ( o * capsule . EncryptOptions ) {
o . Policy , o . Words = capsule . TimeAndKey , wordkey . Normalize ( "perro luna casa" )
} , "at least 6 different words" } ,
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
{ "an instant in the past" , [ ] capsule . Source { source ( "a" , "" ) } , func ( o * capsule . EncryptOptions ) { o . Now = time . Now } , "is not in the future" } ,
} {
opts := files3 ( t )
if tc . edit != nil {
tc . edit ( & opts )
}
var dkc bytes . Buffer
_ , err := capsule . EncryptFiles ( & dkc , tc . sources , opts )
switch {
case err == nil || ! strings . Contains ( err . Error ( ) , tc . want ) :
t . Errorf ( "%s: %v, want %q" , tc . name , err , tc . want )
case dkc . Len ( ) != 0 :
t . Errorf ( "%s: %d bytes written" , tc . name , dkc . Len ( ) )
}
}
}
// changing is a file whose second reading differs from the first.
func changing ( first , second string ) capsule . Source {
n := 0
return capsule . Source { Path : "a.txt" , Size : int64 ( len ( first ) ) , Open : func ( ) ( io . ReadCloser , error ) {
n ++
if n == 1 {
return io . NopCloser ( strings . NewReader ( first ) ) , nil
}
return io . NopCloser ( strings . NewReader ( second ) ) , nil
} }
}
// Spec §62.1 rules 9 and 18: a file that changes between the two readings
// makes EncryptFiles fail; what it wrote must be discarded.
func TestEncryptFilesChangedFile ( t * testing . T ) {
for _ , tc := range [ ] struct {
name string
first , second string
want string
} {
{ "another byte" , "abc" , "abd" , "changed after its first reading: its SHA-256 is another" } ,
{ "shorter" , "abc" , "ab" , "changed after its first reading: 2 bytes, not its size of 3" } ,
{ "longer" , "abc" , "abcd" , "changed after its first reading: more than its size of 3 bytes" } ,
} {
if _ , err := capsule . EncryptFiles ( io . Discard , [ ] capsule . Source { changing ( tc . first , tc . second ) } , files3 ( t ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , tc . want ) {
t . Errorf ( "%s: %v" , tc . name , err )
}
}
}
// Spec §62.1 rule 1: only a generator of test vectors writes format 2, and
// format 2 has no head.
func TestEncryptIsForTestVectors ( t * testing . T ) {
opts := files3 ( t )
opts . Length = 1
var dkc bytes . Buffer
if _ , err := capsule . Encrypt ( & dkc , strings . NewReader ( "x" ) , opts ) ; err == nil || dkc . Len ( ) != 0 {
t . Errorf ( "format 2 without TestVectors: %v" , err )
}
opts . TestVectors , opts . Comment = true , "c"
if _ , err := capsule . Encrypt ( & dkc , strings . NewReader ( "x" ) , opts ) ; err == nil || dkc . Len ( ) != 0 {
t . Errorf ( "format 2 with a comment: %v" , err )
}
opts . Comment = ""
if res , err := capsule . Encrypt ( & dkc , strings . NewReader ( "x" ) , opts ) ; err != nil || res . Format != capsule . Format2 || res . Head != nil {
t . Errorf ( "format 2 for test vectors: %v" , err )
}
}
// Spec §29.4, §54: an unknown critical extension of the head is written as
// given, and the reader that does not know it fails at step 17.
func TestEncryptFilesHeadCritical ( t * testing . T ) {
opts := files3 ( t )
opts . HeadCritical = [ ] extension . Extension { { ID : "org.example.required" , Version : 1 } }
var dkc bytes . Buffer
if _ , err := capsule . EncryptFiles ( & dkc , [ ] capsule . Source { source ( "a" , "a" ) } , opts ) ; err != nil {
t . Fatal ( err )
}
r := open3 ( t , dkc . Bytes ( ) , & testkit . MemorySink { } )
expectStep ( t , "unknown critical extension of the head" , failedStep ( t , r . opened . Inspection . Checks , r . err ) , r . err , datekeys . ErrExtensionCriticalUnknown , 17 )
}