mirror of https://github.com/perkeep/perkeep.git
Support for "planned permanodes" in pkg/jsonsign, pkg/schema and cmd/camput.
A planned permanode involves setting the contents of the permanode (instead of a random string) as well as the OpenPGP signing time so the resultant bytes of the blob (and thus its blobref) is deterministic. This allows multiple independent devices (my laptops) to create the same permanodes for the same files (photos backed up from my phone) when offline (airplane) and then when they sync later, still only have one permanode per unique file. This means that tagging and other metadata applied to permanodes on one laptop merge cleanly with metadata from the other.
This commit is contained in:
parent
8c293e34b6
commit
51e88cac65
|
@ -19,15 +19,19 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"camlistore.org/pkg/client"
|
||||
"camlistore.org/pkg/schema"
|
||||
)
|
||||
|
||||
type permanodeCmd struct {
|
||||
name string
|
||||
tag string
|
||||
name string
|
||||
tag string
|
||||
key string // else random
|
||||
sigTime string
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -35,6 +39,8 @@ func init() {
|
|||
cmd := new(permanodeCmd)
|
||||
flags.StringVar(&cmd.name, "name", "", "Optional name attribute to set on new permanode")
|
||||
flags.StringVar(&cmd.tag, "tag", "", "Optional tag(s) to set on new permanode; comma separated.")
|
||||
flags.StringVar(&cmd.key, "key", "", "Optional key to create deterministic ('planned') permanodes. Must also use --sigtime.")
|
||||
flags.StringVar(&cmd.sigTime, "sigtime", "", "Optional time to put in the OpenPGP signature packet instead of the current time. Required when producing a deterministic permanode (with --key). In format YYYY-MM-DD HH:MM:SS")
|
||||
return cmd
|
||||
})
|
||||
}
|
||||
|
@ -59,7 +65,20 @@ func (c *permanodeCmd) RunCommand(up *Uploader, args []string) error {
|
|||
permaNode *client.PutResult
|
||||
err error
|
||||
)
|
||||
permaNode, err = up.UploadNewPermanode()
|
||||
if (c.key != "") != (c.sigTime != "") {
|
||||
return errors.New("Both --key and --sigtime must be used to produce deterministic permanodes.")
|
||||
}
|
||||
if c.key == "" {
|
||||
// Normal case, with a random permanode.
|
||||
permaNode, err = up.UploadNewPermanode()
|
||||
} else {
|
||||
const format = "2006-01-02 15:04:05"
|
||||
sigTime, err := time.Parse(format, c.sigTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing time %q; expecting time of form %q", c.sigTime, format)
|
||||
}
|
||||
permaNode, err = up.UploadPlannedPermanode(c.key, sigTime)
|
||||
}
|
||||
if handleResult("permanode", permaNode, err) != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"camlistore.org/pkg/blobref"
|
||||
"camlistore.org/pkg/blobserver"
|
||||
|
@ -55,7 +56,9 @@ type Uploader struct {
|
|||
fs http.FileSystem // virtual filesystem to read from; nil means OS filesystem.
|
||||
}
|
||||
|
||||
func (up *Uploader) SignMap(m map[string]interface{}) (string, error) {
|
||||
// sigTime optionally specifies the signature time.
|
||||
// If zero, the current time is used.
|
||||
func (up *Uploader) SignMap(m map[string]interface{}, sigTime time.Time) (string, error) {
|
||||
camliSigBlobref := up.Client.SignerPublicKeyBlobref()
|
||||
if camliSigBlobref == nil {
|
||||
// TODO: more helpful error message
|
||||
|
@ -71,6 +74,7 @@ func (up *Uploader) SignMap(m map[string]interface{}) (string, error) {
|
|||
UnsignedJSON: unsigned,
|
||||
Fetcher: up.Client.GetBlobFetcher(),
|
||||
EntityFetcher: up.entityFetcher,
|
||||
SignatureTime: sigTime,
|
||||
}
|
||||
return sr.Sign()
|
||||
}
|
||||
|
@ -84,7 +88,7 @@ func (up *Uploader) UploadMap(m map[string]interface{}) (*client.PutResult, erro
|
|||
}
|
||||
|
||||
func (up *Uploader) UploadAndSignMap(m map[string]interface{}) (*client.PutResult, error) {
|
||||
signed, err := up.SignMap(m)
|
||||
signed, err := up.SignMap(m, time.Time{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -111,3 +115,12 @@ func (up *Uploader) UploadNewPermanode() (*client.PutResult, error) {
|
|||
unsigned := schema.NewUnsignedPermanode()
|
||||
return up.UploadAndSignMap(unsigned)
|
||||
}
|
||||
|
||||
func (up *Uploader) UploadPlannedPermanode(key string, sigTime time.Time) (*client.PutResult, error) {
|
||||
unsigned := schema.NewPlannedPermanode(key)
|
||||
signed, err := up.SignMap(unsigned, sigTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return up.uploadString(signed)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"camlistore.org/pkg/blobref"
|
||||
|
@ -177,6 +178,9 @@ type SignRequest struct {
|
|||
Fetcher interface{} // blobref.Fetcher or blobref.StreamingFetcher
|
||||
ServerMode bool // if true, can't use pinentry or gpg-agent, etc.
|
||||
|
||||
// Optional signature time. If zero, time.Now() is used.
|
||||
SignatureTime time.Time
|
||||
|
||||
// Optional function to return an entity (including decrypting
|
||||
// the PrivateKey, if necessary)
|
||||
EntityFetcher EntityFetcher
|
||||
|
@ -269,7 +273,7 @@ func (sr *SignRequest) Sign() (signedJSON string, err error) {
|
|||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = openpgp.ArmoredDetachSign(&buf, signer, strings.NewReader(trimmedJSON))
|
||||
err = openpgp.ArmoredDetachSignAt(&buf, signer, sr.SignatureTime, strings.NewReader(trimmedJSON))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -29,6 +29,18 @@ type Entity struct {
|
|||
PrivateKey *packet.PrivateKey
|
||||
Identities map[string]*Identity // indexed by Identity.Name
|
||||
Subkeys []Subkey
|
||||
|
||||
// Clock optionally specifies an alternate clock function to
|
||||
// use when signing or encrypting using this Entity, instead
|
||||
// of time.Now().
|
||||
Clock func() time.Time
|
||||
}
|
||||
|
||||
func (e *Entity) timeNow() time.Time {
|
||||
if e.Clock != nil {
|
||||
return e.Clock()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// An Identity represents an identity claimed by an Entity and zero or more
|
||||
|
|
|
@ -21,54 +21,61 @@ import (
|
|||
// DetachSign signs message with the private key from signer (which must
|
||||
// already have been decrypted) and writes the signature to w.
|
||||
func DetachSign(w io.Writer, signer *Entity, message io.Reader) error {
|
||||
return detachSign(w, signer, message, packet.SigTypeBinary)
|
||||
return detachSign(w, signer, message, time.Time{}, packet.SigTypeBinary)
|
||||
}
|
||||
|
||||
// ArmoredDetachSign signs message with the private key from signer (which
|
||||
// must already have been decrypted) and writes an armored signature to w.
|
||||
func ArmoredDetachSign(w io.Writer, signer *Entity, message io.Reader) (err error) {
|
||||
return armoredDetachSign(w, signer, message, packet.SigTypeBinary)
|
||||
return armoredDetachSign(w, signer, message, time.Time{}, packet.SigTypeBinary)
|
||||
}
|
||||
|
||||
func ArmoredDetachSignAt(w io.Writer, signer *Entity, sigTime time.Time, message io.Reader) (err error) {
|
||||
return armoredDetachSign(w, signer, message, sigTime, packet.SigTypeBinary)
|
||||
}
|
||||
|
||||
// DetachSignText signs message (after canonicalising the line endings) with
|
||||
// the private key from signer (which must already have been decrypted) and
|
||||
// writes the signature to w.
|
||||
func DetachSignText(w io.Writer, signer *Entity, message io.Reader) error {
|
||||
return detachSign(w, signer, message, packet.SigTypeText)
|
||||
return detachSign(w, signer, message, time.Time{}, packet.SigTypeText)
|
||||
}
|
||||
|
||||
// ArmoredDetachSignText signs message (after canonicalising the line endings)
|
||||
// with the private key from signer (which must already have been decrypted)
|
||||
// and writes an armored signature to w.
|
||||
func ArmoredDetachSignText(w io.Writer, signer *Entity, message io.Reader) error {
|
||||
return armoredDetachSign(w, signer, message, packet.SigTypeText)
|
||||
return armoredDetachSign(w, signer, message, time.Time{}, packet.SigTypeText)
|
||||
}
|
||||
|
||||
func armoredDetachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.SignatureType) (err error) {
|
||||
func armoredDetachSign(w io.Writer, signer *Entity, message io.Reader, sigTime time.Time, sigType packet.SignatureType) (err error) {
|
||||
out, err := armor.Encode(w, SignatureType, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = detachSign(out, signer, message, sigType)
|
||||
err = detachSign(out, signer, message, sigTime, sigType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.SignatureType) (err error) {
|
||||
func detachSign(w io.Writer, signer *Entity, message io.Reader, sigTime time.Time, sigType packet.SignatureType) (err error) {
|
||||
if signer.PrivateKey == nil {
|
||||
return errors.InvalidArgumentError("signing key doesn't have a private key")
|
||||
}
|
||||
if signer.PrivateKey.Encrypted {
|
||||
return errors.InvalidArgumentError("signing key is encrypted")
|
||||
}
|
||||
if sigTime.IsZero() {
|
||||
sigTime = time.Now()
|
||||
}
|
||||
|
||||
sig := new(packet.Signature)
|
||||
sig.SigType = sigType
|
||||
sig.PubKeyAlgo = signer.PrivateKey.PubKeyAlgo
|
||||
sig.Hash = crypto.SHA256
|
||||
sig.CreationTime = time.Now()
|
||||
sig.CreationTime = sigTime
|
||||
sig.IssuerKeyId = &signer.PrivateKey.KeyId
|
||||
|
||||
h, wrappedHash, err := hashForSignature(sig.Hash, sig.SigType)
|
||||
|
|
Loading…
Reference in New Issue