Refactor api (#6)
* Refactor library, remove duplicates * Rebuild structure to use Messages and Signature models * Use PGPSplitMessage * Remove signature model * Various fixes * Add helpers with tests * Fixes, add some docs, add tests * Add attachment helpers * Add helpers Symmetric encryption * Edit docs + examples * Rename kr to keyRing * Various fixes for documentation * Edit JSON handling functions, add decrypt keyring via token * Add proposal changes doc * Fix CI * Drop *Message functions, join CleartextMessage and BinaryMessage * Change canonicalization and trimming only to text signatures * Add cleartextsignature, detach signature from message model, move helpers * Documentation, remove optional parameters * Move verification to separate model * Don't return message in VerifyDetached * Update table of contents in readme * Appease golint * Run go fmt * Rename Encrypt/DecryptMessageWithPassword to ..WithToken These functions shouldn't be used with user-provided passwords, as they don't do any key-stretching. * Change key generation usernames
This commit is contained in:
parent
82d49bf235
commit
e65ed17b41
34 changed files with 2573 additions and 1478 deletions
22
helper/base_test.go
Normal file
22
helper/base_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
func readTestFile(name string, trimNewlines bool) string {
|
||||
data, err := ioutil.ReadFile("../crypto/testdata/" + name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if trimNewlines {
|
||||
return strings.TrimRight(string(data), "\n")
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// Corresponding key in ../crypto/testdata/keyring_privateKey
|
||||
const testMailboxPassword = "apple"
|
||||
82
helper/cleartext.go
Normal file
82
helper/cleartext.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/armor"
|
||||
"github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/internal"
|
||||
)
|
||||
|
||||
// SignCleartextMessageArmored signs text given a private key and its passphrase, canonicalizes and trims the newlines,
|
||||
// and returns the PGP-compliant special armoring
|
||||
func SignCleartextMessageArmored(privateKey, passphrase, text string) (string, error) {
|
||||
signingKeyRing, err := pgp.BuildKeyRingArmored(privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = signingKeyRing.UnlockWithPassphrase(passphrase)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return SignCleartextMessage(signingKeyRing, text)
|
||||
}
|
||||
|
||||
// VerifyCleartextMessageArmored verifies PGP-compliant armored signed plain text given the public key
|
||||
// and returns the text or err if the verification fails
|
||||
func VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) {
|
||||
verifyKeyRing, err := pgp.BuildKeyRingArmored(publicKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return VerifyCleartextMessage(verifyKeyRing, armored, verifyTime)
|
||||
}
|
||||
|
||||
// SignCleartextMessage signs text given a private keyring, canonicalizes and trims the newlines,
|
||||
// and returns the PGP-compliant special armoring
|
||||
func SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) {
|
||||
text = canonicalizeAndTrim(text)
|
||||
message := crypto.NewPlainMessageFromString(text)
|
||||
|
||||
signature, err := keyRing.SignDetached(message)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return armor.ArmorClearSignedMessage(message.GetBinary(), signature.GetBinary())
|
||||
}
|
||||
|
||||
// VerifyCleartextMessage verifies PGP-compliant armored signed plain text given the public keyring
|
||||
// and returns the text or err if the verification fails
|
||||
func VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) {
|
||||
text, signatureData, err := armor.ReadClearSignedMessage(armored)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
message := crypto.NewPlainMessageFromString(text)
|
||||
signature := crypto.NewPGPSignature(signatureData)
|
||||
ver, err := keyRing.VerifyDetached(message, signature, verifyTime)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !ver.IsValid() {
|
||||
return "", errors.New("gopenpgp: unable to verify attachment")
|
||||
}
|
||||
|
||||
return message.GetString(), nil
|
||||
}
|
||||
|
||||
// ----- INTERNAL FUNCTIONS -----
|
||||
|
||||
// canonicalizeAndTrim alters a string canonicalizing and trimming the newlines
|
||||
func canonicalizeAndTrim(text string) string {
|
||||
text = internal.TrimNewlines(text)
|
||||
text = strings.Replace(strings.Replace(text, "\r\n", "\n", -1), "\n", "\r\n", -1)
|
||||
return text
|
||||
}
|
||||
45
helper/cleartext_test.go
Normal file
45
helper/cleartext_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const signedPlainText = "Signed message\n"
|
||||
const testTime = 1557754627 // 2019-05-13T13:37:07+00:00
|
||||
var signedMessageTest = regexp.MustCompile(
|
||||
"(?s)^-----BEGIN PGP SIGNED MESSAGE-----.*-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$")
|
||||
|
||||
func TestSignClearText(t *testing.T) {
|
||||
// Password defined in base_test
|
||||
armored, err := SignCleartextMessageArmored(
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword,
|
||||
signedPlainText,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Cannot armor message:", err)
|
||||
}
|
||||
|
||||
assert.Regexp(t, signedMessageTest, armored)
|
||||
|
||||
verified, err := VerifyCleartextMessageArmored(
|
||||
readTestFile("keyring_publicKey", false),
|
||||
armored,
|
||||
pgp.GetUnixTime(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("Cannot verify message:", err)
|
||||
}
|
||||
|
||||
assert.Exactly(t, canonicalizeAndTrim(signedPlainText), verified)
|
||||
}
|
||||
|
||||
func TestMessageCanonicalizeAndTrim(t *testing.T) {
|
||||
text := "Hi \ntest!\r\n\n"
|
||||
canon := canonicalizeAndTrim(text)
|
||||
assert.Exactly(t, "Hi\r\ntest!\r\n\r\n", canon)
|
||||
}
|
||||
258
helper/helper.go
Normal file
258
helper/helper.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/constants"
|
||||
"github.com/ProtonMail/gopenpgp/crypto"
|
||||
)
|
||||
|
||||
var pgp = crypto.GetGopenPGP()
|
||||
|
||||
// EncryptMessageWithToken encrypts a string with a passphrase using AES256
|
||||
func EncryptMessageWithToken(
|
||||
passphrase, plaintext string,
|
||||
) (ciphertext string, err error) {
|
||||
return EncryptMessageWithTokenAlgo(passphrase, plaintext, constants.AES256)
|
||||
}
|
||||
|
||||
// EncryptMessageWithTokenAlgo encrypts a string with a random token and an algorithm chosen from constants.*
|
||||
func EncryptMessageWithTokenAlgo(
|
||||
token, plaintext, algo string,
|
||||
) (ciphertext string, err error) {
|
||||
var pgpMessage *crypto.PGPMessage
|
||||
|
||||
var message = crypto.NewPlainMessageFromString(plaintext)
|
||||
var key = crypto.NewSymmetricKeyFromToken(token, algo)
|
||||
|
||||
if pgpMessage, err = key.Encrypt(message); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ciphertext, err = pgpMessage.GetArmored(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// DecryptMessageWithToken decrypts an armored message with a random token.
|
||||
// The algorithm is derived from the armoring.
|
||||
func DecryptMessageWithToken(token, ciphertext string) (plaintext string, err error) {
|
||||
var message *crypto.PlainMessage
|
||||
var pgpMessage *crypto.PGPMessage
|
||||
|
||||
var key = crypto.NewSymmetricKeyFromToken(token, "")
|
||||
|
||||
if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if message, err = key.Decrypt(pgpMessage); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return message.GetString(), nil
|
||||
}
|
||||
|
||||
// EncryptMessageArmored generates an armored PGP message given a plaintext and an armored public key
|
||||
func EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) {
|
||||
var publicKeyRing *crypto.KeyRing
|
||||
var pgpMessage *crypto.PGPMessage
|
||||
|
||||
var message = crypto.NewPlainMessageFromString(plaintext)
|
||||
|
||||
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pgpMessage, err = publicKeyRing.Encrypt(message, nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ciphertext, err = pgpMessage.GetArmored(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// EncryptSignMessageArmored generates an armored signed PGP message given a plaintext and an armored public key
|
||||
// a private key and its passphrase
|
||||
func EncryptSignMessageArmored(
|
||||
publicKey, privateKey, passphrase, plaintext string,
|
||||
) (ciphertext string, err error) {
|
||||
var publicKeyRing, privateKeyRing *crypto.KeyRing
|
||||
var pgpMessage *crypto.PGPMessage
|
||||
|
||||
var message = crypto.NewPlainMessageFromString(plaintext)
|
||||
|
||||
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pgpMessage, err = publicKeyRing.Encrypt(message, privateKeyRing); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ciphertext, err = pgpMessage.GetArmored(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// DecryptMessageArmored decrypts an armored PGP message given a private key and its passphrase
|
||||
func DecryptMessageArmored(
|
||||
privateKey, passphrase, ciphertext string,
|
||||
) (plaintext string, err error) {
|
||||
var privateKeyRing *crypto.KeyRing
|
||||
var pgpMessage *crypto.PGPMessage
|
||||
var message *crypto.PlainMessage
|
||||
|
||||
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if message, _, err = privateKeyRing.Decrypt(pgpMessage, nil, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return message.GetString(), nil
|
||||
}
|
||||
|
||||
// DecryptVerifyMessageArmored decrypts an armored PGP message given a private key and its passphrase
|
||||
// and verifies the embedded signature.
|
||||
// Returns the plain data or an error on signature verification failure.
|
||||
func DecryptVerifyMessageArmored(
|
||||
publicKey, privateKey, passphrase, ciphertext string,
|
||||
) (plaintext string, err error) {
|
||||
var publicKeyRing, privateKeyRing *crypto.KeyRing
|
||||
var pgpMessage *crypto.PGPMessage
|
||||
var message *crypto.PlainMessage
|
||||
var verification *crypto.Verification
|
||||
|
||||
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if message, verification, err = privateKeyRing.Decrypt(pgpMessage, publicKeyRing, pgp.GetUnixTime()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !verification.IsValid() {
|
||||
return "", errors.New("gopenpgp: unable to verify message")
|
||||
}
|
||||
|
||||
return message.GetString(), nil
|
||||
}
|
||||
|
||||
// EncryptSignAttachment encrypts an attachment using a detached signature, given a publicKey, a privateKey
|
||||
// and its passphrase, the filename, and the unencrypted file data.
|
||||
// Returns keypacket, dataPacket and unarmored (!) signature separate.
|
||||
func EncryptSignAttachment(
|
||||
publicKey, privateKey, passphrase, fileName string,
|
||||
plainData []byte,
|
||||
) (keyPacket, dataPacket, signature []byte, err error) {
|
||||
var publicKeyRing, privateKeyRing *crypto.KeyRing
|
||||
var packets *crypto.PGPSplitMessage
|
||||
var signatureObj *crypto.PGPSignature
|
||||
|
||||
var binMessage = crypto.NewPlainMessage(plainData)
|
||||
|
||||
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if packets, err = publicKeyRing.EncryptAttachment(binMessage, fileName); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if signatureObj, err = privateKeyRing.SignDetached(binMessage); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return packets.GetKeyPacket(), packets.GetDataPacket(), signatureObj.GetBinary(), nil
|
||||
}
|
||||
|
||||
// DecryptVerifyAttachment decrypts and verifies an attachment split into the keyPacket, dataPacket
|
||||
// and an armored (!) signature, given a publicKey, and a privateKey with its passphrase.
|
||||
// Returns the plain data or an error on signature verification failure.
|
||||
func DecryptVerifyAttachment(
|
||||
publicKey, privateKey, passphrase string,
|
||||
keyPacket, dataPacket []byte,
|
||||
armoredSignature string,
|
||||
) (plainData []byte, err error) {
|
||||
var publicKeyRing, privateKeyRing *crypto.KeyRing
|
||||
var detachedSignature *crypto.PGPSignature
|
||||
var message *crypto.PlainMessage
|
||||
var verification *crypto.Verification
|
||||
|
||||
var packets = crypto.NewPGPSplitMessage(keyPacket, dataPacket)
|
||||
|
||||
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if detachedSignature, err = crypto.NewPGPSignatureFromArmored(armoredSignature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if message, err = privateKeyRing.DecryptAttachment(packets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if verification, err = publicKeyRing.VerifyDetached(message, detachedSignature, pgp.GetUnixTime()); err != nil {
|
||||
return nil, errors.New("gopenpgp: unable to verify attachment")
|
||||
}
|
||||
|
||||
if !verification.IsValid() {
|
||||
return nil, errors.New("gopenpgp: unable to verify attachment")
|
||||
}
|
||||
|
||||
return message.GetBinary(), nil
|
||||
}
|
||||
131
helper/helper_test.go
Normal file
131
helper/helper_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAESEncryption(t *testing.T) {
|
||||
var plaintext = "Symmetric secret"
|
||||
var passphrase = "passphrase"
|
||||
|
||||
ciphertext, err := EncryptMessageWithToken(passphrase, plaintext)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when encrypting, got:", err)
|
||||
}
|
||||
|
||||
_, err = DecryptMessageWithToken("Wrong passphrase", ciphertext)
|
||||
assert.EqualError(t, err, "gopenpgp: wrong password in symmetric decryption")
|
||||
|
||||
decrypted, err := DecryptMessageWithToken(passphrase, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when decrypting, got:", err)
|
||||
}
|
||||
|
||||
assert.Exactly(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestArmoredTextMessageEncryption(t *testing.T) {
|
||||
var plaintext = "Secret message"
|
||||
|
||||
armored, err := EncryptMessageArmored(readTestFile("keyring_publicKey", false), plaintext)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when encrypting, got:", err)
|
||||
}
|
||||
|
||||
assert.Exactly(t, true, pgp.IsPGPMessage(armored))
|
||||
|
||||
decrypted, err := DecryptMessageArmored(
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword, // Password defined in base_test
|
||||
armored,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when decrypting, got:", err)
|
||||
}
|
||||
|
||||
assert.Exactly(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestArmoredTextMessageEncryptionVerification(t *testing.T) {
|
||||
var plaintext = "Secret message"
|
||||
|
||||
armored, err := EncryptSignMessageArmored(
|
||||
readTestFile("keyring_publicKey", false),
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword, // Password defined in base_test
|
||||
plaintext,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when encrypting, got:", err)
|
||||
}
|
||||
|
||||
assert.Exactly(t, true, pgp.IsPGPMessage(armored))
|
||||
|
||||
_, err = DecryptVerifyMessageArmored(
|
||||
readTestFile("mime_publicKey", false), // Wrong public key
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword, // Password defined in base_test
|
||||
armored,
|
||||
)
|
||||
assert.EqualError(t, err, "gopenpgp: unable to verify message")
|
||||
|
||||
decrypted, err := DecryptVerifyMessageArmored(
|
||||
readTestFile("keyring_publicKey", false),
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword, // Password defined in base_test
|
||||
armored,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when decrypting, got:", err)
|
||||
}
|
||||
|
||||
assert.Exactly(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestAttachmentEncryptionVerification(t *testing.T) {
|
||||
var attachment = []byte("Secret file\r\nRoot password:hunter2")
|
||||
|
||||
keyPacket, dataPacket, signature, err := EncryptSignAttachment(
|
||||
readTestFile("keyring_publicKey", false),
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword, // Password defined in base_test
|
||||
"password.txt",
|
||||
attachment,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when encrypting, got:", err)
|
||||
}
|
||||
|
||||
sig := crypto.NewPGPSignature(signature)
|
||||
armoredSig, err := sig.GetArmored()
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when armoring signature, got:", err)
|
||||
}
|
||||
|
||||
_, err = DecryptVerifyAttachment(
|
||||
readTestFile("mime_publicKey", false), // Wrong public key
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword, // Password defined in base_test
|
||||
keyPacket,
|
||||
dataPacket,
|
||||
armoredSig,
|
||||
)
|
||||
assert.EqualError(t, err, "gopenpgp: unable to verify attachment")
|
||||
|
||||
decrypted, err := DecryptVerifyAttachment(
|
||||
readTestFile("keyring_publicKey", false),
|
||||
readTestFile("keyring_privateKey", false),
|
||||
testMailboxPassword, // Password defined in base_test
|
||||
keyPacket,
|
||||
dataPacket,
|
||||
armoredSig,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when decrypting, got:", err)
|
||||
}
|
||||
|
||||
assert.Exactly(t, attachment, decrypted)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue