Skip to content
This repository has been archived by the owner on Aug 5, 2021. It is now read-only.

Error: Bad MAC #41

Closed
ivanlemeshev opened this issue Feb 6, 2018 · 12 comments
Closed

Error: Bad MAC #41

ivanlemeshev opened this issue Feb 6, 2018 · 12 comments

Comments

@ivanlemeshev
Copy link

ivanlemeshev commented Feb 6, 2018

I tried to write code to encrypt and decrypt the message. But every time I get an error Error: Bad MAC at libsignal-protocol.js:35296. Here is my code. Tell me please, what's wrong?

const generateKeys = async (preKeyId, signedPreKeyId) => {
    const keys = {};

    const keyHelper = libsignal.KeyHelper;

    const registrationId = await keyHelper.generateRegistrationId();
    keys.registrationId = registrationId;

    const identityKeyPair = await keyHelper.generateIdentityKeyPair();
    keys.identityKeyPair = identityKeyPair;

    const preKey = await keyHelper.generatePreKey(preKeyId);
    keys.preKey = preKey;

    const signedPreKey = await keyHelper.generateSignedPreKey(keys.identityKeyPair, signedPreKeyId);
    keys.signedPreKey = signedPreKey;

    return keys;
};

const serverStorage = {};

const aliceRecipientId = 1;
const aliceDeviceId = '053289b8-e0dd-11e7-a7c1-02420a00000b';
const aliceStorage = new SignalProtocolStore();

const bobRecipientId = 2;
const bobDeviceId = '053289b8-e0dd-11e7-a7c1-02420a000001';
const bobStorage = new SignalProtocolStore();

Promise.all([generateKeys(1, 2), generateKeys(3, 4)])
    // Alice and Bob initialize web clients
    .then(results => {
        console.log('Alice stores keys in the secure local storage');
        aliceStorage.put('identityKey', results[0].identityKeyPair);
        aliceStorage.put('registrationId', results[0].registrationId);
        aliceStorage.storePreKey(results[0].preKey.keyId, results[0].preKey.keyPair);
        aliceStorage.storeSignedPreKey(results[0].signedPreKey.keyId, results[0].signedPreKey.keyPair);

        console.log('Alice sends public pre keys to the server');
        serverStorage.alice = {
            identityKey: results[0].identityKeyPair.pubKey,
            signedPreKey: {
                keyId: results[0].signedPreKey.keyId,
                publicKey: results[0].signedPreKey.keyPair.pubKey,
                signature: results[0].signedPreKey.signature,
            },
            preKey: {
                keyId: results[0].preKey.keyId,
                publicKey: results[0].preKey.keyPair.pubKey,
            }
        };

        console.log('Bob stores keys in the secure local storage');
        bobStorage.put('identityKey', results[1].identityKeyPair);
        bobStorage.put('registrationId', results[1].registrationId);
        bobStorage.storePreKey(results[1].preKey.keyId, results[0].preKey.keyPair);
        bobStorage.storeSignedPreKey(results[1].signedPreKey.keyId, results[1].signedPreKey.keyPair);

        console.log('Bob sends public pre keys to the server');
        serverStorage.bob = {
            identityKey: results[1].identityKeyPair.pubKey,
            signedPreKey: {
                keyId: results[1].signedPreKey.keyId,
                publicKey: results[1].signedPreKey.keyPair.pubKey,
                signature: results[1].signedPreKey.signature,
            },
            preKey: {
                keyId: results[1].preKey.keyId,
                publicKey: results[1].preKey.keyPair.pubKey,
            }
        };
    })
    // Alice tries to create session with Bob to send a message
    .then(() => {
        console.log("Alice gets Bob's public pre keys from the server");
        const bobPublicPreKeys = serverStorage.bob;
        const bobAddress = new libsignal.SignalProtocolAddress(bobDeviceId, bobRecipientId);

        console.log('Alice creates session with Bob');
        const aliceSessionBuilder = new libsignal.SessionBuilder(aliceStorage, bobAddress);

        // Process a prekey fetched from the server.
        // Returns a promise that resolves once a session is created and saved
        // in the store, or rejects if the identityKey differs from a previously
        // seen identity for this address.
        return aliceSessionBuilder.processPreKey({
            registrationId: aliceStorage.getLocalRegistrationId(),
            identityKey: bobPublicPreKeys.identityKey,
            signedPreKey: bobPublicPreKeys.signedPreKey,
            preKey: bobPublicPreKeys.preKey,
        });
    })
    // Alice tries to encrypt and send a message to Bob
    .then(() => {
        console.log('Alise encrypts new message');
        const bobAddress = new libsignal.SignalProtocolAddress(bobDeviceId, bobRecipientId);
        const aliceSessionCipher = new libsignal.SessionCipher(aliceStorage, bobAddress);
        const message = util.toArrayBuffer('Hello Bob!');
        // const message = 'Hello Bob!';
        return aliceSessionCipher.encrypt(message).then(ciphertext => {
            console.log('ciphertext', ciphertext);
            console.log('Alice sends encrypted message to Bob');
            // sourceDevice, destinationDevice, ciphertext
            return ciphertext;
        });
    })
    // Bob receives the encrypted message from the server
    .then(ciphertext => {
        console.log('Bob gets encrypted message from the server');
        const aliceAddress = new libsignal.SignalProtocolAddress(aliceDeviceId, aliceRecipientId);
        const bobSessionCipher = new libsignal.SessionCipher(bobStorage, aliceAddress);

        if (ciphertext.type === 3) { // PREKEY_BUNDLE
            // Decrypt a PreKeyWhisperMessage by first establishing a new session.
            // Returns a promise that resolves when the message is decrypted or
            // rejects if the identityKey differs from a previously seen identity for this
            // address.
            bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext.body, 'binary')
                .then(plaintext => {
                    console.log('decrypted message', plaintext);
                }).catch(error => {
                    console.log('decrypted error', error);
                });
        } else {
            bobSessionCipher.decryptWhisperMessage(ciphertext.body, 'binary')
                .then(plaintext => {
                    console.log('decrypted message', plaintext);
                }).catch(error => {
                    console.log('decrypted error', error);
                });
        }
    })
    .catch(error => {
        console.log('error', error);
    });
@ivanlemeshev
Copy link
Author

I found my error in the code.

@matthewroche
Copy link

Hi Ivan. Are you able to remember what was wrong with your code? I'm getting the exact same error implementing an extremely basic version of signal:

    let user1 = {}
    let user2 = {}

    //Create stores
    user1.store = new SignalProtocolStore();
    user2.store = new SignalProtocolStore();

    //Create registration IDs
    user1.registrationId = KeyHelper.generateRegistrationId();
    user1.store.put('registrationId', user1.registrationId);
    user2.registrationId = KeyHelper.generateRegistrationId();
    user2.store.put('registrationId', user2.registrationId);

    //Generate identity key pairs
    user1.identityKeyPair = await KeyHelper.generateIdentityKeyPair()
    user1.store.put('identityKey', user1.identityKeyPair);
    user2.identityKeyPair = await KeyHelper.generateIdentityKeyPair()
    user2.store.put('identityKey', user2.identityKeyPair);

    //Generate signed pre-key
    user1.signedPreKey = await KeyHelper.generateSignedPreKey(user1.identityKeyPair, 0)
    user1.store.storeSignedPreKey(0, user1.signedPreKey.keyPair)
    user2.signedPreKey = await KeyHelper.generateSignedPreKey(user2.identityKeyPair, 0)
    user2.store.storeSignedPreKey(0, user2.signedPreKey.keyPair)

    //Generate pre-key
    user1.preKey = await KeyHelper.generatePreKey(0)
    user1.store.storePreKey(0, user1.preKey.keyPair);
    user2.preKey = await KeyHelper.generatePreKey(0)
    user2.store.storePreKey(0, user2.preKey.keyPair);

    //Create session
    user1.session = {}
    user1.session.address = new libsignal.SignalProtocolAddress(user2.registrationId, 1);
    user1.session.sessionBuilder = new libsignal.SessionBuilder(user1.store, user1.session.address);
    await user1.session.sessionBuilder.processPreKey({
      registrationId: user2.registrationId,
      identityKey: user2.identityKeyPair.pubKey,
      signedPreKey: {
          keyId     : user2.signedPreKey.keyId,
          publicKey : user2.signedPreKey.keyPair.pubKey,
          signature : user2.signedPreKey.signature
      },
      preKey: {
          keyId     : user2.preKey.keyId,
          publicKey : user2.preKey.keyPair.pubKey
      }
    });
    
    //Encrypt message
    user1.message = {}
    user1.message.address = new libsignal.SignalProtocolAddress(user2.registrationId, 1);
    user1.message.sessionCipher = new libsignal.SessionCipher(user1.store, user1.message.address);
    user1.message.cipherText = await user1.message.sessionCipher.encrypt("A message from user 1")
    //Encode message as base 64 (Avoids issues with JS encoding)
    user1.message.cipherText.body = window.btoa(user1.message.cipherText.body)

    //Decrypt message
    user2.message = {}
    user2.message.address = new libsignal.SignalProtocolAddress(user2.registrationId, 1);
    user2.message.sessionCipher = new libsignal.SessionCipher(user2.store, user2.message.address);
    user2.message.plainText = ""
    if (user1.message.cipherText.type === 3) {
      user2.message.plainText = await user2.message.sessionCipher.decryptPreKeyWhisperMessage(user1.message.cipherText.body, 'base64')
    } else {
      user2.message.plainText = await user2.message.sessionCipher.decryptWhisperMessage(user1.message.cipherText.body, 'base64')
    }
    
    console.log(user2.message.plainText)

Error: Bad MAC at libsignal-protocol.js:35294:27

@changeweb
Copy link

@ivanlemeshev @matthewroche Could you share the fix of the error?

@matthewroche
Copy link

Hi @changeweb

The library will not accept preKeys or signedPreKeys with an id of 0. All that is required to make the code I posted above work is to change the keyIds to 1. The only other change I have made to the working code below is to convert the final ArrayBuffer output to a string.

    let user1 = {}
    let user2 = {}

    //Create stores
    user1.store = new SignalProtocolStore();
    user2.store = new SignalProtocolStore();

    //Create registration IDs
    user1.registrationId = KeyHelper.generateRegistrationId();
    user1.store.put('registrationId', user1.registrationId);
    user2.registrationId = KeyHelper.generateRegistrationId();
    user2.store.put('registrationId', user2.registrationId);

    //Generate identity key pairs
    user1.identityKeyPair = await KeyHelper.generateIdentityKeyPair()
    user1.store.put('identityKey', user1.identityKeyPair);
    user2.identityKeyPair = await KeyHelper.generateIdentityKeyPair()
    user2.store.put('identityKey', user2.identityKeyPair);

    //Generate signed pre-key
    user1.signedPreKey = await KeyHelper.generateSignedPreKey(user1.identityKeyPair, 1)
    user1.store.storeSignedPreKey(1, user1.signedPreKey.keyPair)
    user2.signedPreKey = await KeyHelper.generateSignedPreKey(user2.identityKeyPair, 1)
    user2.store.storeSignedPreKey(1, user2.signedPreKey.keyPair)

    //Generate pre-key
    user1.preKey = await KeyHelper.generatePreKey(1)
    user1.store.storePreKey(1, user1.preKey.keyPair);
    user2.preKey = await KeyHelper.generatePreKey(1)
    user2.store.storePreKey(1, user2.preKey.keyPair);

    //Create session
    user1.session = {}
    user1.session.address = new libsignal.SignalProtocolAddress(user2.registrationId, 1);
    user1.session.sessionBuilder = new libsignal.SessionBuilder(user1.store, user1.session.address);
    await user1.session.sessionBuilder.processPreKey({
      registrationId: user2.registrationId,
      identityKey: user2.identityKeyPair.pubKey,
      signedPreKey: {
          keyId     : user2.signedPreKey.keyId,
          publicKey : user2.signedPreKey.keyPair.pubKey,
          signature : user2.signedPreKey.signature
      },
      preKey: {
          keyId     : user2.preKey.keyId,
          publicKey : user2.preKey.keyPair.pubKey
      }
    });

    //Encrypt message
    user1.message = {}
    user1.message.address = new libsignal.SignalProtocolAddress(user2.registrationId, 1);
    user1.message.sessionCipher = new libsignal.SessionCipher(user1.store, user1.message.address);
    user1.message.cipherText = await user1.message.sessionCipher.encrypt("A message from user 1")
    //Encode message as base 64 (Avoids issues with JS encoding)
    user1.message.cipherText.body = window.btoa(user1.message.cipherText.body)

    //Decrypt message
    user2.message = {}
    user2.message.address = new libsignal.SignalProtocolAddress(user2.registrationId, 1);
    user2.message.sessionCipher = new libsignal.SessionCipher(user2.store, user2.message.address);
    user2.message.plainText = ""
    if (user1.message.cipherText.type === 3) {
      user2.message.plainText = await user2.message.sessionCipher.decryptPreKeyWhisperMessage(user1.message.cipherText.body, 'base64')
    } else {
      user2.message.plainText = await user2.message.sessionCipher.decryptWhisperMessage(user1.message.cipherText.body, 'base64')
    }

    console.log(util.toString(user2.message.plainText))

It looks like you've already found the Simple-Signal-Protocol-Demonstration library I created. I'll update the readme so that it's clear how to get this up and running though.

@changeweb
Copy link

@matthewroche thanks for your response. Yes I found the library. It was very helpful.

@1111mp
Copy link

1111mp commented Nov 19, 2020

@matthewroche Can I ask you a question. It’s introduced in the document: 'This protocol uses a concept called 'PreKeys'. A PreKey is an ECPublicKey and an associated unique ID which are stored together by a server. PreKeys can also be signed.

At install time, clients generate a single signed PreKey, as well as a large list of unsigned PreKeys, and transmit all of them to the server.'.
Does this preKey generate a new one for encryption every time a message is sent?
In other words, each client only needs to generate the key once during installation and save it.

@matthewroche
Copy link

@1111mp I'm afraid I'm not working with this package anymore so my knowledge is a bit rusty. From memory each client needs to generate one signed prekey and multiple unsigned prekeys at install time, save it locally and send the public parts to the server.

The unsigned prekeys are used to create a session, and can only be used once. You therefore need to regularly check that there are sufficient prekeys available on the server and create new ones once you fall below a threshold.

The signed prekey is static, but I think for optimal encryption security is supposed to be rotated within a certain time interval.

These keys are only used to set up the session. The actual keys used to encrypt the messages are created within the session and are updated with each message. Using this package means you don't need to worry about any of that though.

Further details for the session set up are found here: https://signal.org/docs/specifications/x3dh/

Hope this helps

@1111mp
Copy link

1111mp commented Nov 19, 2020

@matthewroche Thank you very much for your reply. I'll go and study X3DH first. Thank you.

@1111mp
Copy link

1111mp commented Nov 19, 2020

@matthewroche Excuse me,how can an encrypted message be decrypted the next session is established?
Such as this code(Shorthand):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Bob</title>
    <script type="text/javascript" src="./libsignal-protocol.js"></script>
    <script type="text/javascript" src="./store.js"></script>
  </head>

  <body>
    <div onclick="getMsg()">button</div>
    <script>
      /** https://github.com/z-950/blog/issues/5
       * 	https://github.com/throneless-tech/libsignal-service-javascript
       * 	https://github.com/signalapp/libsignal-protocol-javascript/issues/41
       */

      var KeyHelper = libsignal.KeyHelper;
      var aliceStore = new SignalProtocolStore();
      var bobStore = new SignalProtocolStore();

      generateKeys(123, function (aliceKeys) {
        aliceStore.put("identityKey", aliceKeys.identityKeyPair);
        aliceStore.put("registrationId", aliceKeys.registrationId);
        aliceStore.storePreKey(
          aliceKeys.preKey.keyId,
          aliceKeys.preKey.keyPair
        );
        aliceStore.storeSignedPreKey(
          aliceKeys.signedPreKey.keyId,
          aliceKeys.signedPreKey.keyPair
        );
        console.log("aliceKeys.registrationId", aliceKeys.registrationId);

        generateKeys(456, function (bobKeys) {
          console.log(bobKeys);
          bobStore.put("identityKey", bobKeys.identityKeyPair);
          bobStore.put("registrationId", bobKeys.registrationId);
          bobStore.storePreKey(bobKeys.preKey.keyId, bobKeys.preKey.keyPair);
          bobStore.storeSignedPreKey(
            bobKeys.signedPreKey.keyId,
            bobKeys.signedPreKey.keyPair
          );

          console.log("bobKeys.registrationId", bobKeys.registrationId);

          var aliceRecipientId = "17621398254";
          var aliceDeviceId = 1;
          /** 接受人的 id 和 设备 id */
          var bobRecipientId = "17601254993";
          var bobDeviceId = 1;

          var bobAddress = new libsignal.SignalProtocolAddress(
            bobRecipientId,
            bobDeviceId
          );

          // Instantiate a SessionBuilder for a remote recipientId + deviceId tuple.
          var sessionBuilder = new libsignal.SessionBuilder(
            aliceStore,
            bobAddress
          );

          // Process a prekey fetched from the server. Returns a promise that resolves
          // once a session is created and saved in the store, or rejects if the
          // identityKey differs from a previously seen identity for this address.
          var promise = sessionBuilder.processPreKey({
            registrationId: aliceKeys.registrationId,
            identityKey: bobKeys.identityKeyPair.pubKey,
            signedPreKey: {
              keyId: bobKeys.signedPreKey.keyId,
              publicKey: bobKeys.signedPreKey.keyPair.pubKey,
              signature: bobKeys.signedPreKey.signature,
            },
            preKey: {
              keyId: bobKeys.preKey.keyId,
              publicKey: bobKeys.preKey.keyPair.pubKey,
            },
          });

          promise.then(function onsuccess() {
            // encrypt messages
            console.log("Vamo a encriptar");
          });

          promise.catch(function onerror(error) {
            // handle identity key conflict
            console.log(error);
          });

          /** alice 向 Bob 发送的 加密的文本 */
          // const plaintext = "Hello world";
          // let ciphertext;
          const sessionCipher = new libsignal.SessionCipher(
            aliceStore,
            bobAddress
          );
          console.log(sessionCipher);

          var aliceAddress = new libsignal.SignalProtocolAddress(
            aliceRecipientId,
            aliceDeviceId
          );
          var bobSessionCipher = (window.bobSessionCipher = new libsignal.SessionCipher(
            bobStore,
            aliceAddress
          ));

          console.log("bobSessionCipher", bobSessionCipher);

          function doing(text) {
            sessionCipher
              .encrypt(text)
              .then(function (ciphertext) {
                // ciphertext -> { type: <Number>, body: <string> }
                console.log("ciphertext:", ciphertext);
                // ciphertext = ciphertext;
                // handle(ciphertext.type, ciphertext.body);
                localStorage.setItem("encrypt", JSON.stringify(ciphertext));
                return ciphertext;
              })
              .then((ciphertext) => {
                // var sessionCipher = new libsignal.SessionCipher(store, address);
                // sessionCipher.decryptWhisperMessage(ciphertext.body).then(function (plaintext) {
                // 	// handle plaintext ArrayBuffer
                // 	console.log(plaintext)
                // });

                /** 然后 Bob 收到之后 开始对文本解密 */
                // const text = JSON.parse(localStorage.getItem("encrypt"));
                // console.log(45646549879879);
                // console.log(text);

                // 首先建立一个新的会话来解密PreKeyWhisperMessage。
                // 返回一个承诺,该承诺将在消息解密时解析,或者如果identityKey与该地址先前看到的身份不同,则拒绝。
                bobSessionCipher
                  .decryptPreKeyWhisperMessage(ciphertext.body, "binary")
                  .then(function (plaintext) {
                    // handle plaintext ArrayBuffer
                    // 处理纯文本ArrayBuffer
                    console.log(plaintext);
                    console.log(util.toString(plaintext));
                  })
                  .catch(function (error) {
                    // handle identity key conflict
                    // 处理身份密钥冲突
                    console.log(error);
                  });
              });
          }

          doing("Hello world");
          // doing('454654sdasdas');
        });
      });

      function generateKeys(keyId, callback) {
        var keys = {};
        keys.registrationId = KeyHelper.generateRegistrationId();
        // Store registrationId somewhere durable and safe.
        KeyHelper.generateIdentityKeyPair().then(function (identityKeyPair) {
          // keyPair -> { pubKey: ArrayBuffer, privKey: ArrayBuffer }
          // Store identityKeyPair somewhere durable and safe.
          keys.identityKeyPair = identityKeyPair;

          KeyHelper.generatePreKey(keyId).then(function (preKey) {
            // store.storePreKey(preKey.keyId, preKey.keyPair);
            keys.preKey = preKey;

            KeyHelper.generateSignedPreKey(identityKeyPair, keyId).then(
              function (signedPreKey) {
                // store.storeSignedPreKey(signedPreKey.keyId, signedPreKey.keyPair);
                keys.signedPreKey = signedPreKey;
                callback(keys);
              }
            );
          });
        });
      }

      function getMsg() {
        const text = JSON.parse(localStorage.getItem("encrypt"));
        console.log(45646549879879);
				console.log(text);
				
        bobSessionCipher
          .decryptPreKeyWhisperMessage(text.body, "binary")
          .then(function (plaintext) {
            // handle plaintext ArrayBuffer
            console.log(plaintext);
            console.log(util.toString(plaintext));
          })
          .catch(function (error) {
            // handle identity key conflict
            console.log(error);
          });
      }
    </script>
  </body>
</html>

When I click on the Button, the decryption fails again.
Can it be decrypted only once? What to do between multiple devices?

@matthewroche
Copy link

I don't have time to load your code and play around with it right now. After a quick overview I think encryption fails because you're trying to decrypt the same message twice. If you create a new message and try to decrypt that I believe it should work.

If you wish to decrypt the same message on multiple devices, each device needs to create its own session, and each device has to have its own keys.

I'm afraid I don't use this library any more, so I'm not best placed to help you. You might be best looking for help at https://community.signalusers.org/

@1111mp
Copy link

1111mp commented Nov 20, 2020

@matthewroche You have helped me a lot, thank you very much. In the case of multiple devices, for example, Alice has two devices A1 and A2, and then Bob sends a message to Alice. At this time, Bob's device should send two messages M1 and M2, encrypted with the public key of A1 and the public key of A2, respectively. Then A1 and A2 receive M1 and M2 respectively, and then use their own keys to decrypt. Is this correct? In other words, you only need to send a message, and then there is a way to support both devices A1 and A2 to decrypt.
I hope I didn't interrupt your rest time. Thank you very much for your help.

@1111mp
Copy link

1111mp commented Nov 20, 2020

@matthewroche I have tried to find answers in the community, but the relevant content is too little, I am really sorry.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants