Skip to content

Commit

Permalink
Write multiple QR code images if data exceeds capacity (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Christian Häusler authored Oct 21, 2018
1 parent f9d4ba6 commit daca659
Show file tree
Hide file tree
Showing 77 changed files with 292 additions and 502 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,15 @@ Restore a GPG key (builds on top of the above example):

## Known issues

The QR code format provide a limited feature set:

* It only works for input data of size 2119 bytes or less.
* It can not be used to recover the data.
The QR code format provide a limited feature set and can not be used to recover
the data. A tool like `zbarimg` from the [zbar libary][zbar] can be used to
scan the qr codes so it can be read by the zbase32 format.

## Milestones

* [x] Basic application
* [x] Plain text format for print and easy scan/ocr
* [ ] QR Code format for easier scanning
* [x] QR Code format for easier scanning
* [ ] Template system for custom output

## Contributing and license
Expand All @@ -64,3 +63,4 @@ to this project, see [CONTRIBUTING.md].
[CONTRIBUTING.md]: https://github.com/corvus-ch/horcrux/blob/master/CONTRIBUTING.md
[MIT]: https://github.com/corvus-ch/horcrux/blob/master/LICENSE
[paperkey]: http://www.jabberwocky.com/software/paperkey/
[zbar]: https://zbar.sourceforge.io
14 changes: 10 additions & 4 deletions format/base64/base64.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/base64"
"fmt"
"io"
"os"
)

// Name holds the name of the Format.
Expand All @@ -24,11 +25,16 @@ func (f *Format) OutputFileName(x byte) string {
return fmt.Sprintf("%s.base64.%03d", f.Stem, x)
}

// Writer creates a new Format writer using the given writer as output.
func (f *Format) Writer(out io.Writer) (io.Writer, []io.Closer, error) {
enc := base64.NewEncoder(base64.StdEncoding, out)
// Writer creates a new base64 format writer for the part identified by x.
func (f *Format) Writer(x byte) (io.Writer, []io.Closer, error) {
file, err := os.Create(f.OutputFileName(x))
if nil != err {
return nil, nil, err
}

return enc, []io.Closer{enc}, nil
enc := base64.NewEncoder(base64.StdEncoding, file)

return enc, []io.Closer{file, enc}, nil
}

// Reader creates a new Format reader using the given reader as input.
Expand Down
34 changes: 8 additions & 26 deletions format/base64/base64_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package base64_test

import (
"fmt"
"path/filepath"
"testing"

"github.com/corvus-ch/horcrux/format"
Expand All @@ -9,40 +11,20 @@ import (
"github.com/stretchr/testify/assert"
)

var nameTests = []formatAssert.NameTest{
{0, "mollis", "mollis.base64.000"},
{1, "commodo", "commodo.base64.001"},
{42, "pellentesque", "pellentesque.base64.042"},
{181, "fringilla", "fringilla.base64.181"},
{254, "venenatis", "venenatis.base64.254"},
{255, "ridiculus", "ridiculus.base64.255"},
}

var dataTests = []formatAssert.DataTest{
{[]byte{0}, "AA=="},
{[]byte{0xff}, "/w=="},
{[]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, "////////"},
{[]byte{
0xc0, 0x73, 0x62, 0x4a, 0xaf, 0x39, 0x78, 0x51,
0x4e, 0xf8, 0x44, 0x3b, 0xb2, 0xa8, 0x59, 0xc7,
0x5f, 0xc3, 0xcc, 0x6a, 0xf2, 0x6d, 0x5a, 0xaa,
}, "wHNiSq85eFFO+EQ7sqhZx1/DzGrybVqq"},
}

func factory(s string) format.Format {
return base64.New(s)
}

func TestFormat_OutputFileName(t *testing.T) {
formatAssert.Name(t, nameTests, factory)
}

func TestFormat_Reader(t *testing.T) {
formatAssert.DataRead(t, dataTests, factory)
formatAssert.DataRead(t, factory, ".base64")
}

func TestFormat_Writer(t *testing.T) {
formatAssert.DataWrite(t, dataTests, factory)
formatAssert.DataWrite(t, factory, ".base64", func(file string, x byte) []string {
base := filepath.Base(file)
name := base[0 : len(base)-len(filepath.Ext(base))]
return []string{fmt.Sprintf("%s.base64.%03d", name, x)}
})
}

func TestFormat_Name(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions format/base64/fixtures/48bit.base64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
////////
1 change: 1 addition & 0 deletions format/base64/fixtures/48bit.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
������
1 change: 1 addition & 0 deletions format/base64/fixtures/8bit.base64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/w==
1 change: 1 addition & 0 deletions format/base64/fixtures/8bit.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1 change: 1 addition & 0 deletions format/base64/fixtures/soup.base64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
Binary file added format/base64/fixtures/soup.bin
Binary file not shown.
1 change: 1 addition & 0 deletions format/base64/fixtures/zero.base64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AA==
Binary file added format/base64/fixtures/zero.bin
Binary file not shown.
26 changes: 6 additions & 20 deletions format/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"io"
"os"

"crypto/rand"
"github.com/bketelsen/logr"
Expand All @@ -31,11 +30,15 @@ func (f *Factory) Create(x byte) (io.Writer, error) {
ws := make([]io.Writer, len(f.formats))

var i int
var err error
for _, format := range f.formats {
if ws[i], err = f.create(x, format); err != nil {
w, c, err := format.Writer(x)
if err != nil {
return nil, err
}
ws[i] = w
if nil != c {
f.c = append(f.c, c...)
}
i++
}

Expand All @@ -46,23 +49,6 @@ func (f *Factory) Create(x byte) (io.Writer, error) {
return f.encryptWriter(io.MultiWriter(ws...), x)
}

func (f *Factory) create(x byte, format Format) (io.Writer, error) {
file, err := os.Create(format.OutputFileName(x))
if nil != err {
return nil, err
}
f.c = append(f.c, file)
w, c, err := format.Writer(file)
if nil != err {
return nil, err
}
if nil != c {
f.c = append(f.c, c...)
}

return w, nil
}

func (f *Factory) encryptWriter(w io.Writer, x byte) (io.Writer, error) {
p, err := generatePassword()
if nil != err {
Expand Down
4 changes: 2 additions & 2 deletions format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type Format interface {
// OutputFileName returns the file name for the given x.
OutputFileName(x byte) string

// Writer creates a new format writer using the given writer as output.
Writer(w io.Writer) (io.Writer, []io.Closer, error)
// Writer creates a new format writer for the part identified by x
Writer(x byte) (io.Writer, []io.Closer, error)

// Reader creates a new format reader using the given reader as input.
Reader(r io.Reader) (io.Reader, error)
Expand Down
93 changes: 74 additions & 19 deletions format/internal/assert/data.go
Original file line number Diff line number Diff line change
@@ -1,44 +1,99 @@
package assert

import (
"bytes"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"

"github.com/corvus-ch/horcrux/format"
"github.com/sebdah/goldie"
"github.com/stretchr/testify/assert"
)

// DataTest holds data required for format data read and write assertions.
type DataTest struct {
Decoded []byte
Encoded string
}
// FormatFactory describes a func used for instantiating a Format during assertions.
type FormatFactory func(string) format.Format

// OutputFileNames describes a func used to get the output file names only known to the calling test case.
type OutputFileNames func(file string, x byte) []string

// DataRead asserts a formats read behaviour.
func DataRead(t *testing.T, tests []DataTest, factory FormatFactory) {
for _, test := range tests {
t.Run(test.Encoded, func(t *testing.T) {
r, err := factory("").Reader(bytes.NewBufferString(test.Encoded))
func DataRead(t *testing.T, factory FormatFactory, suffix string) {
goldie.FileNameSuffix = ".bin"
files, err := filepath.Glob(filepath.Join(goldie.FixtureDir, "*"+suffix))
if err != nil {
t.Fatal(err)
}
for _, file := range files {
basename := filepath.Base(file)
name := strings.TrimSuffix(basename, filepath.Ext(basename))
t.Run(name, func(t *testing.T) {
f, err := os.Open(file)
if err != nil {
t.Fatal(err)
}
r, err := factory("").Reader(f)
assert.Nil(t, err)
out, err := ioutil.ReadAll(r)
assert.NoError(t, err)
assert.Equal(t, test.Decoded, out)
goldie.Assert(t, name, out)
})
}
}

// DataWrite asserts a formats write behaviour.
func DataWrite(t *testing.T, tests []DataTest, factory FormatFactory) {
for _, test := range tests {
t.Run(test.Encoded, func(t *testing.T) {
out := &bytes.Buffer{}
w, cl, err := factory("").Writer(out)
func DataWrite(t *testing.T, factory FormatFactory, suffix string, outfilenames OutputFileNames) {
goldie.FileNameSuffix = suffix
files, err := filepath.Glob(filepath.Join(goldie.FixtureDir, "*.bin"))
if err != nil {
t.Fatal(err)
}
for _, file := range files {
basename := filepath.Base(file)
name := strings.TrimSuffix(basename, filepath.Ext(basename))
t.Run(name, func(t *testing.T) {
x := randomByte(t)
f, err := os.Open(file)
if err != nil {
t.Fatal(err)
}
dir, err := ioutil.TempDir("", name)
defer os.RemoveAll(dir)
if err != nil {
t.Fatal(err)
}
subject := factory(filepath.Join(dir, name))
w, cl, err := subject.Writer(x)
assert.NoError(t, err)
_, err = io.Copy(w, f)
assert.NoError(t, err)
w.Write(test.Decoded)
for i := len(cl); i > 0; i-- {
cl[i-1].Close()
err = cl[i-1].Close()
assert.NoError(t, err)
}
for _, outfile := range outfilenames(file, x) {
file, err := os.Open(filepath.Join(dir, outfile))
if err != nil {
t.Fatal(err)
}
out, _ := ioutil.ReadAll(file)
goldenName := strings.Replace(strings.Replace(outfile, suffix, "", -1), fmt.Sprintf(".%03d", x), "", -1)
t.Log(goldenName)
goldie.Assert(t, goldenName, out)
}
assert.Equal(t, test.Encoded, out.String())
})
}
}

func randomByte(t *testing.T) byte {
b := make([]byte, 1)
if _, err := rand.Read(b); err != nil {
t.Fatal(err)
}

return b[0]
}
6 changes: 0 additions & 6 deletions format/internal/assert/factory.go

This file was deleted.

25 changes: 0 additions & 25 deletions format/internal/assert/name.go

This file was deleted.

File renamed without changes
1 change: 1 addition & 0 deletions format/qr/fixtures/48bit.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
������
Binary file not shown.
File renamed without changes
1 change: 1 addition & 0 deletions format/qr/fixtures/8bit.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Binary file not shown.
Binary file added format/qr/fixtures/double.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added format/qr/fixtures/double.2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added format/qr/fixtures/double.bin
Binary file not shown.
Binary file added format/qr/fixtures/soup.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added format/qr/fixtures/soup.bin
Binary file not shown.
Binary file added format/qr/fixtures/tripple.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added format/qr/fixtures/tripple.2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added format/qr/fixtures/tripple.3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added format/qr/fixtures/tripple.bin
Binary file not shown.
File renamed without changes
Binary file added format/qr/fixtures/zero.bin
Binary file not shown.
17 changes: 7 additions & 10 deletions format/qr/qr.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,30 @@ var encoding = zbase32.NewEncoding(alphabet)
// New creates a new Format of type raw.
func New(stem string) *Format {
return &Format{
stem: stem,
Stem: stem,
Level: qr.M,
Size: 500,
}
}

// Format represents the raw type format.
type Format struct {
stem string
Stem string
Level qr.ErrorCorrectionLevel
Size int
}

// OutputFileName returns the file name for the given x.
func (f *Format) OutputFileName(x byte) string {
return fmt.Sprintf("%s.%03d.png", f.stem, x)
return fmt.Sprintf("%s.png.%03d", f.Stem, x)
}

// Writer creates a new Format writer using the given writer as output.
func (f *Format) Writer(out io.Writer) (io.Writer, []io.Closer, error) {
cs := make([]io.Closer, 2)
w := NewWriter(out, f)
cs[0] = w
// Writer creates a new QR code format writer for the part identified by x.
func (f *Format) Writer(x byte) (io.Writer, []io.Closer, error) {
w := NewWriter(f, x)
enc := zbase32.NewEncoder(encoding, w)
cs[1] = enc

return enc, cs, nil
return enc, []io.Closer{w, enc}, nil
}

// Reader creates a new Format reader using the given reader as input.
Expand Down
Loading

0 comments on commit daca659

Please sign in to comment.