diff --git a/crypto/attachment.go b/crypto/attachment.go index b078ce1c..7ba442c6 100644 --- a/crypto/attachment.go +++ b/crypto/attachment.go @@ -85,7 +85,7 @@ func (keyRing *KeyRing) newAttachmentProcessor( config := &packet.Config{ DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), + Time: GetTime, } reader, writer := io.Pipe() @@ -163,7 +163,7 @@ func (keyRing *KeyRing) DecryptAttachment(message *PGPSplitMessage) (*PlainMessa encryptedReader := io.MultiReader(keyReader, dataReader) - config := &packet.Config{Time: getTimeGenerator()} + config := &packet.Config{Time: GetTime} md, err := openpgp.ReadMessage(encryptedReader, privKeyEntries, nil, config) if err != nil { diff --git a/crypto/attachment_manual.go b/crypto/attachment_manual.go index 39a2957a..b2a3b17e 100644 --- a/crypto/attachment_manual.go +++ b/crypto/attachment_manual.go @@ -95,7 +95,7 @@ func (keyRing *KeyRing) NewManualAttachmentProcessor( // encryption config config := &packet.Config{ DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), + Time: GetTime, } // goroutine that reads the key packet diff --git a/crypto/attachment_manual_test.go b/crypto/attachment_manual_test.go index 4a9673df..ad88d9e0 100644 --- a/crypto/attachment_manual_test.go +++ b/crypto/attachment_manual_test.go @@ -9,8 +9,9 @@ import ( ) func TestManualAttachmentProcessor(t *testing.T) { - pgp.latestServerTime = 1615394034 - defer func() { pgp.latestServerTime = testTime }() + defer setFixedTime(testTime) + setFixedTime(1615394034) + passphrase := []byte("wUMuF/lkDPYWH/0ZqqY8kJKw7YJg6kS") pk, err := NewKeyFromArmored(readTestFile("att_key", false)) if err != nil { @@ -86,8 +87,8 @@ func TestManualAttachmentProcessor(t *testing.T) { } func TestManualAttachmentProcessorNotEnoughBuffer(t *testing.T) { - pgp.latestServerTime = 1615394034 - defer func() { pgp.latestServerTime = testTime }() + defer setFixedTime(testTime) + setFixedTime(1615394034) passphrase := []byte("wUMuF/lkDPYWH/0ZqqY8kJKw7YJg6kS") pk, err := NewKeyFromArmored(readTestFile("att_key", false)) if err != nil { @@ -141,8 +142,9 @@ func TestManualAttachmentProcessorNotEnoughBuffer(t *testing.T) { } func TestManualAttachmentProcessorEmptyBuffer(t *testing.T) { - pgp.latestServerTime = 1615394034 - defer func() { pgp.latestServerTime = testTime }() + defer setFixedTime(testTime) + setFixedTime(1615394034) + passphrase := []byte("wUMuF/lkDPYWH/0ZqqY8kJKw7YJg6kS") pk, err := NewKeyFromArmored(readTestFile("att_key", false)) if err != nil { diff --git a/crypto/base_test.go b/crypto/base_test.go index 72c92c16..cd80e94e 100644 --- a/crypto/base_test.go +++ b/crypto/base_test.go @@ -27,7 +27,7 @@ func readTestFile(name string, trimNewlines bool) string { } func init() { - UpdateTime(testTime) // 2019-05-13T13:37:07+00:00 + setFixedTime(testTime) // 2019-05-13T13:37:07+00:00 initGenerateKeys() initArmoredKeys() diff --git a/crypto/gopenpgp.go b/crypto/gopenpgp.go index d5ae56d5..55b32910 100644 --- a/crypto/gopenpgp.go +++ b/crypto/gopenpgp.go @@ -6,13 +6,15 @@ import "sync" // GopenPGP is used as a "namespace" for many of the functions in this package. // It is a struct that keeps track of time skew between server and client. type GopenPGP struct { - latestServerTime int64 + fixedTime int64 + timeOffset int64 generationOffset int64 lock *sync.RWMutex } var pgp = GopenPGP{ - latestServerTime: 0, + fixedTime: 0, + timeOffset: 0, generationOffset: 0, lock: &sync.RWMutex{}, } diff --git a/crypto/key.go b/crypto/key.go index 16c50d98..73e51542 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -264,26 +264,26 @@ func (key *Key) GetPublicKey() (b []byte, err error) { // CanVerify returns true if any of the subkeys can be used for verification. func (key *Key) CanVerify() bool { - _, canVerify := key.entity.SigningKey(getNow()) + _, canVerify := key.entity.SigningKey(GetTime()) return canVerify } // CanEncrypt returns true if any of the subkeys can be used for encryption. func (key *Key) CanEncrypt() bool { - _, canEncrypt := key.entity.EncryptionKey(getNow()) + _, canEncrypt := key.entity.EncryptionKey(GetTime()) return canEncrypt } // IsExpired checks whether the key is expired. func (key *Key) IsExpired() bool { i := key.entity.PrimaryIdentity() - return key.entity.PrimaryKey.KeyExpired(i.SelfSignature, getNow()) || // primary key has expired - i.SelfSignature.SigExpired(getNow()) // user ID self-signature has expired + return key.entity.PrimaryKey.KeyExpired(i.SelfSignature, GetTime()) || // primary key has expired + i.SelfSignature.SigExpired(GetTime()) // user ID self-signature has expired } // IsRevoked checks whether the key or the primary identity has a valid revocation signature. func (key *Key) IsRevoked() bool { - return key.entity.Revoked(getNow()) || key.entity.PrimaryIdentity().Revoked(getNow()) + return key.entity.Revoked(GetTime()) || key.entity.PrimaryIdentity().Revoked(GetTime()) } // IsPrivate returns true if the key is private. @@ -447,7 +447,7 @@ func generateKey( cfg := &packet.Config{ Algorithm: packet.PubKeyAlgoRSA, RSABits: bits, - Time: getKeyGenerationTimeGenerator(), + Time: getKeyGenerationTime, DefaultHash: crypto.SHA256, DefaultCipher: packet.CipherAES256, DefaultCompressionAlgo: packet.CompressionZLIB, diff --git a/crypto/key_test.go b/crypto/key_test.go index e80c6c0b..dc467fdf 100644 --- a/crypto/key_test.go +++ b/crypto/key_test.go @@ -221,6 +221,42 @@ func TestIsExpired(t *testing.T) { assert.Exactly(t, true, futureKey.IsExpired()) } +func TestGeneratedWithOffset(t *testing.T) { + defer setFixedTime(testTime) + setFixedTime(0) + defer SetTimeOffset(0) + var timeOffset int64 = 30 + SetTimeOffset(timeOffset) + + // generate key with offset + keyTestRSA, err := GenerateKey(keyTestName, keyTestDomain, "rsa", 1024) + if err != nil { + panic("Cannot generate RSA key:" + err.Error()) + } + + // Bring back offset to zero + SetTimeOffset(0) + + // Verify if key was generated with offset but lower by 1 sec to compensate time passing in test + assert.GreaterOrEqual(t, keyTestRSA.entity.PrimaryKey.CreationTime.Unix(), GetUnixTime()+timeOffset-1) +} + +func TestGeneratedWithKeyOffset(t *testing.T) { + defer setFixedTime(testTime) + setFixedTime(testTime) + defer SetKeyGenerationOffset(0) + var timeOffset int64 = 30 + SetKeyGenerationOffset(timeOffset) + + // generate key with key offset + keyTestRSA, err := GenerateKey(keyTestName, keyTestDomain, "rsa", 1024) + if err != nil { + panic("Cannot generate RSA key:" + err.Error()) + } + + assert.GreaterOrEqual(t, keyTestRSA.entity.PrimaryKey.CreationTime.Unix(), testTime+timeOffset) +} + func TestGenerateKeyWithPrimes(t *testing.T) { prime1, _ := base64.StdEncoding.DecodeString( "/thF8zjjk6fFx/y9NId35NFx8JTA7jvHEl+gI0dp9dIl9trmeZb+ESZ8f7bNXUmTI8j271kyenlrVJiqwqk80Q==") @@ -418,10 +454,8 @@ func TestKeyCapabilities(t *testing.T) { } func TestRevokedKeyCapabilities(t *testing.T) { - pgp.latestServerTime = 1632219895 - defer func() { - pgp.latestServerTime = testTime - }() + defer setFixedTime(testTime) + setFixedTime(1632219895) revokedKey, err := NewKeyFromArmored(readTestFile("key_revoked", false)) if err != nil { diff --git a/crypto/keyring_message.go b/crypto/keyring_message.go index f0fc16e0..1927f742 100644 --- a/crypto/keyring_message.go +++ b/crypto/keyring_message.go @@ -241,7 +241,7 @@ func asymmetricEncryptStream( ) (encryptWriter io.WriteCloser, err error) { config := &packet.Config{ DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), + Time: GetTime, } if compress { @@ -337,7 +337,7 @@ func asymmetricDecryptStream( but the caller will remove signature expiration errors later on. See processSignatureExpiration(). */ - return getNow() + return GetTime() } return time.Unix(verifyTime, 0) }, diff --git a/crypto/keyring_session.go b/crypto/keyring_session.go index ca2a94f9..a8a91536 100644 --- a/crypto/keyring_session.go +++ b/crypto/keyring_session.go @@ -80,7 +80,7 @@ func (keyRing *KeyRing) EncryptSessionKey(sk *SessionKey) ([]byte, error) { pubKeys := make([]*packet.PublicKey, 0, len(keyRing.entities)) for _, e := range keyRing.entities { - encryptionKey, ok := e.EncryptionKey(getNow()) + encryptionKey, ok := e.EncryptionKey(GetTime()) if !ok { return nil, errors.New("gopenpgp: encryption key is unavailable for key id " + strconv.FormatUint(e.PrimaryKey.KeyId, 16)) } diff --git a/crypto/keyring_test.go b/crypto/keyring_test.go index cbfb4bed..e16ab668 100644 --- a/crypto/keyring_test.go +++ b/crypto/keyring_test.go @@ -236,11 +236,10 @@ func TestKeyringCapabilities(t *testing.T) { } func TestVerificationTime(t *testing.T) { + defer setFixedTime(testTime) + setFixedTime(1632312383) message := NewPlainMessageFromString("Hello") - pgp.latestServerTime = 1632312383 - defer func() { - pgp.latestServerTime = testTime - }() + enc, err := keyRingTestPublic.Encrypt( message, keyRingTestPrivate, diff --git a/crypto/message_test.go b/crypto/message_test.go index 906955a4..c4a1b836 100644 --- a/crypto/message_test.go +++ b/crypto/message_test.go @@ -211,10 +211,8 @@ func TestBinaryMessageEncryption(t *testing.T) { } func TestIssue11(t *testing.T) { - pgp.latestServerTime = 1559655272 - defer func() { - pgp.latestServerTime = testTime - }() + defer setFixedTime(testTime) + setFixedTime(1559655272) var issue11Password = []byte("1234") @@ -258,8 +256,8 @@ func TestIssue11(t *testing.T) { } func TestDummy(t *testing.T) { - pgp.latestServerTime = 1636644417 - defer func() { pgp.latestServerTime = testTime }() + defer setFixedTime(testTime) + setFixedTime(1636644417) dummyKey, err := NewKeyFromArmored(readTestFile("key_dummy", false)) if err != nil { diff --git a/crypto/mime.go b/crypto/mime.go index d756dfcc..dc9867cb 100644 --- a/crypto/mime.go +++ b/crypto/mime.go @@ -86,7 +86,10 @@ func parseMIME( if err != nil { return nil, nil, nil, errors.Wrap(err, "gopenpgp: error in reading message") } - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: getTimeGenerator()} + config := &packet.Config{ + DefaultCipher: packet.CipherAES256, + Time: GetTime, + } h := textproto.MIMEHeader(mm.Header) mmBodyData, err := ioutil.ReadAll(mm.Body) diff --git a/crypto/password.go b/crypto/password.go index 64ee0264..2c581d87 100644 --- a/crypto/password.go +++ b/crypto/password.go @@ -93,6 +93,7 @@ func EncryptSessionKeyWithPassword(sk *SessionKey, password []byte) ([]byte, err config := &packet.Config{ DefaultCipher: cf, + Time: GetTime, } err = packet.SerializeSymmetricKeyEncryptedReuseKey(outbuf, sk.Key, password, config) @@ -109,7 +110,7 @@ func passwordEncrypt(message *PlainMessage, password []byte) ([]byte, error) { config := &packet.Config{ DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), + Time: GetTime, } hints := &openpgp.FileHints{ @@ -148,7 +149,7 @@ func passwordDecrypt(encryptedIO io.Reader, password []byte) (*PlainMessage, err } config := &packet.Config{ - Time: getTimeGenerator(), + Time: GetTime, } var emptyKeyRing openpgp.EntityList diff --git a/crypto/sessionkey.go b/crypto/sessionkey.go index 1e48209d..70a73e47 100644 --- a/crypto/sessionkey.go +++ b/crypto/sessionkey.go @@ -71,7 +71,10 @@ func (sk *SessionKey) GetBase64Key() string { // RandomToken generates a random token with the specified key size. func RandomToken(size int) ([]byte, error) { - config := &packet.Config{DefaultCipher: packet.CipherAES256} + config := &packet.Config{ + DefaultCipher: packet.CipherAES256, + Time: GetTime, + } symKey := make([]byte, size) if _, err := io.ReadFull(config.Random(), symKey); err != nil { return nil, errors.Wrap(err, "gopenpgp: error in generating random token") @@ -225,7 +228,7 @@ func encryptStreamWithSessionKey( } config := &packet.Config{ - Time: getTimeGenerator(), + Time: GetTime, DefaultCipher: dc, } @@ -426,7 +429,7 @@ func decryptStreamWithSessionKey( } config := &packet.Config{ - Time: getTimeGenerator(), + Time: GetTime, } if verificationContext != nil { diff --git a/crypto/signature.go b/crypto/signature.go index f5c58ab3..222bd80f 100644 --- a/crypto/signature.go +++ b/crypto/signature.go @@ -306,7 +306,7 @@ func signMessageDetached( ) (*PGPSignature, error) { config := &packet.Config{ DefaultHash: crypto.SHA512, - Time: getTimeGenerator(), + Time: GetTime, } signEntity, err := signKeyRing.getSigningEntity() diff --git a/crypto/signature_test.go b/crypto/signature_test.go index de32e1f9..47c5ccd6 100644 --- a/crypto/signature_test.go +++ b/crypto/signature_test.go @@ -139,12 +139,11 @@ func TestVerifyBinDetachedSig(t *testing.T) { } func Test_KeyRing_GetVerifiedSignatureTimestampSuccess(t *testing.T) { - message := NewPlainMessageFromString(testMessage) + defer setFixedTime(testTime) var time int64 = 1600000000 - pgp.latestServerTime = time - defer func() { - pgp.latestServerTime = testTime - }() + setFixedTime(time) + message := NewPlainMessageFromString(testMessage) + signature, err := keyRingTestPrivate.SignDetached(message) if err != nil { t.Errorf("Got an error while generating the signature: %v", err) @@ -159,12 +158,10 @@ func Test_KeyRing_GetVerifiedSignatureTimestampSuccess(t *testing.T) { } func Test_KeyRing_GetVerifiedSignatureTimestampWithContext(t *testing.T) { - message := NewPlainMessageFromString(testMessage) + defer setFixedTime(testTime) var time int64 = 1600000000 - pgp.latestServerTime = time - defer func() { - pgp.latestServerTime = testTime - }() + setFixedTime(time) + message := NewPlainMessageFromString(testMessage) var testContext = "test-context" signature, err := keyRingTestPrivate.SignDetachedWithContext(message, NewSigningContext(testContext, true)) if err != nil { @@ -259,12 +256,10 @@ func getTimestampOfIssuer(signature *PGPSignature, keyID uint64) int64 { } func Test_KeyRing_GetVerifiedSignatureTimestampError(t *testing.T) { - message := NewPlainMessageFromString(testMessage) + defer setFixedTime(testTime) var time int64 = 1600000000 - pgp.latestServerTime = time - defer func() { - pgp.latestServerTime = testTime - }() + setFixedTime(time) + message := NewPlainMessageFromString(testMessage) signature, err := keyRingTestPrivate.SignDetached(message) if err != nil { t.Errorf("Got an error while generating the signature: %v", err) @@ -616,12 +611,13 @@ func Test_VerifyDetachedWithDoubleContext(t *testing.T) { } func Test_verifySignaturExpire(t *testing.T) { - defer func(t int64) { pgp.latestServerTime = t }(pgp.latestServerTime) - pgp.latestServerTime = 0 + defer setFixedTime(testTime) + setFixedTime(0) const lifetime = uint32(time.Hour / time.Second) cfg := &packet.Config{ + Time: GetTime, Algorithm: packet.PubKeyAlgoEdDSA, DefaultHash: crypto.SHA256, DefaultCipher: packet.CipherAES256, @@ -655,10 +651,7 @@ func Test_verifySignaturExpire(t *testing.T) { sig := NewPGPSignature(signature.GetBinary()) - // packet.PublicKey.KeyExpired will return false here because PublicKey CreationTime has - // nanosecond precision, while pgpcrypto.GetUnixTime() has only second precision. - // Adjust the check time to be in the future to ensure that the key is not expired. - err = keyRing.VerifyDetached(message, sig, GetUnixTime()+1) + err = keyRing.VerifyDetached(message, sig, GetUnixTime()) if err != nil { t.Fatal(err) } diff --git a/crypto/time.go b/crypto/time.go index b208d874..246cf26c 100644 --- a/crypto/time.go +++ b/crypto/time.go @@ -4,16 +4,26 @@ import ( "time" ) -// UpdateTime updates cached time. +// UpdateTime updates cached time, new time has to be after previously set or will be ignored otherwise. +// Calling this function will cause time used in all crypto operations to be constant equal to provided. func UpdateTime(newTime int64) { pgp.lock.Lock() defer pgp.lock.Unlock() - if newTime > pgp.latestServerTime { - pgp.latestServerTime = newTime + if pgp.fixedTime < newTime { + pgp.fixedTime = newTime } } +// SetTimeOffset updates time offset used for crypto operations. +// Offset will be applied to all crypto operations unless fixed time is used. +func SetTimeOffset(newOffset int64) { + pgp.lock.Lock() + defer pgp.lock.Unlock() + + pgp.timeOffset = newOffset +} + // SetKeyGenerationOffset updates the offset when generating keys. func SetKeyGenerationOffset(offset int64) { pgp.lock.Lock() @@ -24,46 +34,39 @@ func SetKeyGenerationOffset(offset int64) { // GetUnixTime gets latest cached time. func GetUnixTime() int64 { - return getNow().Unix() + return GetTime().Unix() } // GetTime gets latest cached time. func GetTime() time.Time { - return getNow() -} - -// ----- INTERNAL FUNCTIONS ----- - -// getNow returns the latest server time. -func getNow() time.Time { pgp.lock.RLock() defer pgp.lock.RUnlock() - if pgp.latestServerTime == 0 { - return time.Now() + if pgp.fixedTime == 0 { + return time.Unix(time.Now().Unix()+pgp.timeOffset, 0) } - return time.Unix(pgp.latestServerTime, 0) + return time.Unix(pgp.fixedTime, 0) } -// getTimeGenerator Returns a time generator function. -func getTimeGenerator() func() time.Time { - return getNow +// ----- INTERNAL FUNCTIONS ----- + +// setFixedTime sets fixed pgp time +func setFixedTime(newTime int64) { + pgp.lock.Lock() + defer pgp.lock.Unlock() + + pgp.fixedTime = newTime } -// getNowKeyGenerationOffset returns the current time with the key generation offset. -func getNowKeyGenerationOffset() time.Time { +// getKeyGenerationTime returns the current time with the key generation offset. +func getKeyGenerationTime() time.Time { pgp.lock.RLock() defer pgp.lock.RUnlock() - if pgp.latestServerTime == 0 { - return time.Unix(time.Now().Unix()+pgp.generationOffset, 0) + if pgp.fixedTime == 0 { + return time.Unix(time.Now().Unix()+pgp.generationOffset+pgp.timeOffset, 0) } - return time.Unix(pgp.latestServerTime+pgp.generationOffset, 0) -} - -// getKeyGenerationTimeGenerator Returns a time generator function with the key generation offset. -func getKeyGenerationTimeGenerator() func() time.Time { - return getNowKeyGenerationOffset + return time.Unix(pgp.fixedTime+pgp.generationOffset, 0) } diff --git a/crypto/time_test.go b/crypto/time_test.go index 50d3ae35..117743a2 100644 --- a/crypto/time_test.go +++ b/crypto/time_test.go @@ -7,11 +7,48 @@ import ( "github.com/stretchr/testify/assert" ) -func TestTime(t *testing.T) { +func Test_setFixedTime(t *testing.T) { + defer setFixedTime(testTime) + setFixedTime(1571072494) + time.Sleep(1 * time.Second) + now := GetUnixTime() + + assert.Exactly(t, int64(1571072494), now) // Use fixed time +} + +func Test_UpdateTime(t *testing.T) { + defer setFixedTime(testTime) UpdateTime(1571072494) time.Sleep(1 * time.Second) now := GetUnixTime() - assert.Exactly(t, int64(1571072494), now) // Use latest server time - UpdateTime(testTime) + assert.Exactly(t, int64(1571072494), now) // Use fixed time + + UpdateTime(1571072490) + now = GetUnixTime() + + assert.Exactly(t, int64(1571072494), now) // Use previous fixed time, ignoring time before previous + + UpdateTime(1571072495) + now = GetUnixTime() + + assert.Exactly(t, int64(1571072495), now) // Use updated fixed time +} + +func Test_TimeOffset(t *testing.T) { + defer setFixedTime(testTime) + defer SetTimeOffset(0) + setFixedTime(testTime) + SetTimeOffset(30) + time.Sleep(1 * time.Second) + now := GetUnixTime() + + assert.Exactly(t, int64(testTime), now) // Use fixed time without offset + + setFixedTime(0) + SetTimeOffset(0) + now = GetUnixTime() + SetTimeOffset(30) + + assert.GreaterOrEqual(t, GetUnixTime(), now+30) // Use offset with no fixed time } diff --git a/helper/base_test.go b/helper/base_test.go index d3fd8c18..9daa4dd1 100644 --- a/helper/base_test.go +++ b/helper/base_test.go @@ -3,8 +3,6 @@ package helper import ( "io/ioutil" "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" ) const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 @@ -24,5 +22,5 @@ func readTestFile(name string, trimNewlines bool) string { var testMailboxPassword = []byte("apple") func init() { - crypto.UpdateTime(testTime) // 2019-05-13T13:37:07+00:00 + crypto.setFixedTime(testTime) // 2019-05-13T13:37:07+00:00 }