mirror of
https://github.com/taigrr/arc
synced 2025-01-18 04:33:13 -08:00
380 lines
7.4 KiB
Go
380 lines
7.4 KiB
Go
// Copyright (C) 2016 - Will Glozer. All rights reserved.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/rand"
|
|
"errors"
|
|
"io"
|
|
|
|
"github.com/codahale/sss"
|
|
"github.com/magical/argon2"
|
|
"github.com/wg/arc/archive"
|
|
"github.com/wg/arc/binary"
|
|
)
|
|
|
|
const (
|
|
Version = 0x01
|
|
Password = 0x01
|
|
Curve448 = 0x02
|
|
Shard = 0x03
|
|
KeySize = archive.KeySize
|
|
)
|
|
|
|
type Archiver interface {
|
|
Reader() (*Reader, error)
|
|
Writer() (*Writer, error)
|
|
}
|
|
|
|
type Reader struct {
|
|
buffer *bufio.Reader
|
|
files []File
|
|
*archive.Reader
|
|
}
|
|
|
|
type Writer struct {
|
|
buffer *bufio.Writer
|
|
files []File
|
|
tagAt int64
|
|
*archive.Writer
|
|
}
|
|
|
|
var (
|
|
ErrInvalidArchive = errors.New("archive: verify failed")
|
|
ErrInvalidVersion = errors.New("archive: unsupported version")
|
|
ErrPasswordArchive = errors.New("archive: password archive")
|
|
ErrCurve448Archive = errors.New("archive: curve448 archive")
|
|
ErrShardArchive = errors.New("archive: shard archive")
|
|
)
|
|
|
|
// A PasswordArchive is encrypted with a key derived from a password,
|
|
// cost parameters, and cryptographically secure random salt using the
|
|
// Argon2 password hashing function.
|
|
type PasswordArchive struct {
|
|
Version byte
|
|
Type byte
|
|
Iterations uint32
|
|
Memory uint32
|
|
Salt [32]byte
|
|
Password []byte
|
|
File File
|
|
}
|
|
|
|
func NewPasswordArchive(password []byte, iterations, memory uint32, file File) *PasswordArchive {
|
|
return &PasswordArchive{
|
|
Version: Version,
|
|
Type: Password,
|
|
Iterations: iterations,
|
|
Memory: memory,
|
|
Password: password,
|
|
File: file,
|
|
}
|
|
}
|
|
|
|
func (a *PasswordArchive) Reader() (*Reader, error) {
|
|
err := binary.Read(a.File, binary.LE, a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch {
|
|
case a.Version != Version:
|
|
return nil, ErrInvalidVersion
|
|
case a.Type == Curve448:
|
|
return nil, ErrCurve448Archive
|
|
case a.Type == Shard:
|
|
return nil, ErrShardArchive
|
|
}
|
|
|
|
key, err := a.Key()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newArchiveReader(key, a.File, a.File)
|
|
}
|
|
|
|
func (a *PasswordArchive) Writer() (*Writer, error) {
|
|
_, err := rand.Read(a.Salt[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = binary.Write(a.File, binary.LE, a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key, err := a.Key()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newArchiveWriter(key, a.File, a.File)
|
|
}
|
|
|
|
func (a *PasswordArchive) Key() ([]byte, error) {
|
|
var (
|
|
password = a.Password
|
|
salt = a.Salt[:]
|
|
iterations = int(a.Iterations)
|
|
memory = int64(a.Memory)
|
|
)
|
|
return argon2.Key(password, salt, iterations, 1, memory, KeySize)
|
|
}
|
|
|
|
// A Curve448Archive is encrypted with a key derived from applying
|
|
// BLAKE2b to the shared secret derived from an X448 ECDH key exchange
|
|
// with an ephemeral private key and static public key.
|
|
type Curve448Archive struct {
|
|
Version byte
|
|
Type byte
|
|
Ephemeral PublicKey
|
|
PublicKey *PublicKey
|
|
PrivateKey *PrivateKey
|
|
File File
|
|
}
|
|
|
|
func NewCurve448Archive(public *PublicKey, private *PrivateKey, file File) *Curve448Archive {
|
|
return &Curve448Archive{
|
|
Version: Version,
|
|
Type: Curve448,
|
|
PublicKey: public,
|
|
PrivateKey: private,
|
|
File: file,
|
|
}
|
|
}
|
|
|
|
func (a *Curve448Archive) Reader() (*Reader, error) {
|
|
err := binary.Read(a.File, binary.LE, a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch {
|
|
case a.Version != Version:
|
|
return nil, ErrInvalidVersion
|
|
case a.Type == Password:
|
|
return nil, ErrPasswordArchive
|
|
case a.Type == Shard:
|
|
return nil, ErrShardArchive
|
|
}
|
|
|
|
key, err := ComputeSharedKey(&a.Ephemeral, a.PrivateKey, KeySize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newArchiveReader(key, a.File, a.File)
|
|
}
|
|
|
|
func (a *Curve448Archive) Writer() (*Writer, error) {
|
|
ephemeralPublicKey, ephemeralPrivateKey, err := GenerateKeypair()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer ephemeralPrivateKey.Zero()
|
|
|
|
key, err := ComputeSharedKey(a.PublicKey, ephemeralPrivateKey, KeySize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.Ephemeral = *ephemeralPublicKey
|
|
err = binary.Write(a.File, binary.LE, a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newArchiveWriter(key, a.File, a.File)
|
|
}
|
|
|
|
// A ShardArchive is encrypted with a key consisting of cryptographically
|
|
// secure random bytes. That key is split into n shards using Shamir's
|
|
// Secret Sharing algorithm and one archive is generate for each shard.
|
|
// k shards must be present to recreate the key.
|
|
type ShardArchive struct {
|
|
Version byte
|
|
Type byte
|
|
ID byte
|
|
Share [KeySize]byte
|
|
Threshold int
|
|
File File
|
|
Shards []*ShardArchive
|
|
}
|
|
|
|
func NewShardArchive(threshold int, files []File) *ShardArchive {
|
|
shards := make([]*ShardArchive, len(files))
|
|
|
|
for i, file := range files {
|
|
shards[i] = &ShardArchive{
|
|
Version: Version,
|
|
Type: Shard,
|
|
Threshold: threshold,
|
|
File: file,
|
|
Shards: shards,
|
|
}
|
|
}
|
|
|
|
return shards[0]
|
|
}
|
|
|
|
func (a *ShardArchive) Reader() (*Reader, error) {
|
|
shares := make(map[byte][]byte, len(a.Shards))
|
|
|
|
for _, shard := range a.Shards {
|
|
err := binary.Read(shard.File, binary.LE, shard)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch {
|
|
case shard.Version != Version:
|
|
return nil, ErrInvalidVersion
|
|
case shard.Type == Password:
|
|
return nil, ErrPasswordArchive
|
|
case shard.Type == Curve448:
|
|
return nil, ErrCurve448Archive
|
|
}
|
|
|
|
shares[shard.ID] = shard.Share[:]
|
|
}
|
|
|
|
key := sss.Combine(shares)
|
|
|
|
return newArchiveReader(key, a.File, a.Files()...)
|
|
}
|
|
|
|
func (a *ShardArchive) Writer() (*Writer, error) {
|
|
var key [32]byte
|
|
|
|
_, err := rand.Read(key[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
n := byte(len(a.Shards))
|
|
k := byte(a.Threshold)
|
|
|
|
shares, err := sss.Split(n, k, key[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
writers := make([]io.Writer, len(a.Shards))
|
|
for id, share := range shares {
|
|
index := id - 1
|
|
shard := a.Shards[index]
|
|
|
|
shard.ID = id
|
|
copy(shard.Share[:], share)
|
|
|
|
err = binary.Write(shard.File, binary.LE, shard)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
writers[index] = shard.File
|
|
}
|
|
w := io.MultiWriter(writers...)
|
|
|
|
return newArchiveWriter(key[:], w, a.Files()...)
|
|
}
|
|
|
|
func (a *ShardArchive) Files() []File {
|
|
files := make([]File, len(a.Shards))
|
|
for i, shard := range a.Shards {
|
|
files[i] = shard.File
|
|
}
|
|
return files
|
|
}
|
|
|
|
func newArchiveReader(key []byte, raw io.Reader, files ...File) (*Reader, error) {
|
|
switch valid, err := verify(key, files[0]); {
|
|
case err != nil:
|
|
return nil, err
|
|
case !valid:
|
|
return nil, ErrInvalidArchive
|
|
}
|
|
|
|
buffer := bufio.NewReader(raw)
|
|
r, err := archive.NewReader(buffer, key)
|
|
|
|
return &Reader{
|
|
Reader: r,
|
|
buffer: buffer,
|
|
files: files,
|
|
}, err
|
|
}
|
|
|
|
func newArchiveWriter(key []byte, raw io.Writer, files ...File) (*Writer, error) {
|
|
tagAt, err := files[0].Seek(0, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buffer := bufio.NewWriter(raw)
|
|
w, err := archive.NewWriter(buffer, key)
|
|
|
|
return &Writer{
|
|
Writer: w,
|
|
buffer: buffer,
|
|
files: files,
|
|
tagAt: tagAt,
|
|
}, err
|
|
}
|
|
|
|
func verify(key []byte, file File) (bool, error) {
|
|
p, err := file.Seek(0, 1)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer file.Seek(p, 0)
|
|
buffer := bufio.NewReader(file)
|
|
return archive.Verify(buffer, key)
|
|
}
|
|
|
|
func (r *Reader) Close() error {
|
|
for _, f := range r.files {
|
|
err := f.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *Writer) Close() error {
|
|
tag, err := w.Finish()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = w.buffer.Flush()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, f := range w.files {
|
|
_, err := f.WriteAt(tag, w.tagAt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = f.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type File interface {
|
|
io.Reader
|
|
io.Writer
|
|
io.WriterAt
|
|
io.Seeker
|
|
io.Closer
|
|
}
|