Skip to content

Commit

Permalink
Add support for decrypting a version-prefixed ciphertext.
Browse files Browse the repository at this point in the history
Some existing implementations prefix the encrypted ciphertext with a
version byte (which is always 0x01 to my knowledge). This commit adds
support for decrypting these payloads.

This change includes an internal API to generate versioned ciphertext
for testing, but it does not add a public one. This could be done in the
future simply by exposing the existing private method.

Signed-off-by: Aaron Jacobs <[email protected]>
  • Loading branch information
atheriel committed Feb 7, 2022
1 parent 80e4ec0 commit 47fbc9d
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 4 deletions.
44 changes: 40 additions & 4 deletions crypt/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,34 @@ func (k *Key) base64String() string {
// Encrypt produces base64-encoded cipher text for the given payload and key, or
// an error if one cannot be created.
func (k *Key) Encrypt(s string) (string, error) {
var nonce [24]byte
_, err := rand.Read(nonce[:])
output, err := k.encryptSecretbox(s)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(output), nil
}

// encryptVersioned produces a base64-encoded cipher text with an embedded
// version for the given payload and key, or an error if one cannot be created.
// This emulates the format used by some implementations.
func (k *Key) encryptVersioned(s string) (string, error) {
output, err := k.encryptSecretbox(s)
if err != nil {
return "", err
}
output = append([]byte{1}, output...)
return base64.StdEncoding.EncodeToString(output), nil
}

func (k *Key) encryptSecretbox(s string) ([]byte, error) {
var nonce [24]byte
_, err := rand.Read(nonce[:])
if err != nil {
return []byte{}, err
}
output := secretbox.Seal(nil, []byte(s), &nonce, k.key32())
output = append(nonce[:], output...)

return base64.StdEncoding.EncodeToString(output), nil
return output, nil
}

// Decrypt takes base64-encoded cipher text encrypted with the given key and
Expand All @@ -123,6 +141,24 @@ func (k *Key) Decrypt(s string) (string, error) {
if err != nil {
return "", fmt.Errorf("invalid decryption payload: %v", err)
}
if len(buf) < 1 {
return "", ErrPayLoadTooShort
}
// Some implementations use a version-prefixed cipher text. In order to
// handle the (unlikely but possible) case where a versionless payload
// *just happens* to start with a valid version byte, we must also try
// the fallback on error.
switch buf[0] {
case byte(1):
str, err := k.decryptSecretbox(buf[1:])
if err == nil {
return str, err
}
}
return k.decryptSecretbox(buf)
}

func (k *Key) decryptSecretbox(buf []byte) (string, error) {
if len(buf) < minimumSecretboxLength {
return "", ErrPayLoadTooShort
}
Expand Down
35 changes: 35 additions & 0 deletions crypt/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,41 @@ func (s *KeySuite) TestEncryption(c *check.C) {
c.Check(err, check.ErrorMatches, `cannot read`)
}

func (s *KeySuite) TestVersionedEncryption(c *check.C) {
key, _ := NewKey()

// Too short to have a version prefix.
_, err := key.Decrypt("")
c.Check(err, check.Equals, ErrPayLoadTooShort)

// A payload encrypted with some other key but with a valid version.
_, err = key.Decrypt("ASnl7KSpgnkA+jyYy2IErhgFL54O2qGvIbYxyoa/to+C1EgeFl/90GXEm15PZPApoOSf8A==")
c.Check(err, check.Equals, ErrFailedToDecrypt)

// Roundtrip encryption test.
cipher, err := key.encryptVersioned("some secret")
c.Check(err, check.IsNil)
c.Check(cipher, check.Not(check.Equals), "some secret") // Just checking.
text, err := key.Decrypt(cipher)
c.Check(err, check.IsNil)
c.Check(text, check.Equals, "some secret")

// Check that nonces actually work.
dupCipher, err := key.encryptVersioned("some secret")
c.Check(err, check.IsNil)
c.Check(dupCipher, check.Not(check.Equals), cipher)

// Swap out the standard library's crypto reader for the remainder of
// the tests so we can simulate a failure to generate random bits.
randReader := rand.Reader
rand.Reader = &errReader{}
defer func() { rand.Reader = randReader }()

_, err = key.encryptVersioned("some secret")
c.Check(err, check.Not(check.IsNil))
c.Check(err, check.ErrorMatches, `cannot read`)
}

func Test(t *testing.T) {
_ = check.Suite(&KeySuite{})
check.TestingT(t)
Expand Down

0 comments on commit 47fbc9d

Please sign in to comment.