z-950 / blog

This is a blog for myself
0 stars 0 forks source link

Signal协议 js使用 #5

Open z-950 opened 3 years ago

z-950 commented 3 years ago

signal协议是一种棘轮式前向保密协议,适用于同步和异步消息传递环境。

signal协议的js实现:libsignal-protocol-javascript

注意

此为例子。该js库的实现不完整,缺少了原始版本的部分方法,见issue

概念

使用

引入dist/libsignal-protocol.js

  1. 装载:生成所需的identity keys, registration id, prekeys
    
    const KeyHelper = libsignal.KeyHelper;

const registrationId = KeyHelper.generateRegistrationId(); // Store registrationId somewhere durable and safe. // store需要自行实现,可参考最后的例子 // 可以储存在浏览器的localStorage或者indexDB。如果浏览器不安全则毫无办法 // 为2.建立会话中同一个store

KeyHelper.generateIdentityKeyPair().then(function(identityKeyPair) { // keyPair -> { pubKey: ArrayBuffer, privKey: ArrayBuffer } // keyPair格式 // Store identityKeyPair somewhere durable and safe. });

KeyHelper.generatePreKey(keyId).then(function(preKey) { store.storePreKey(preKey.keyId, preKey.keyPair); });

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

// Register preKeys and signedPreKey with the server // 向服务器注册,服务器有signal相关接口. // 应为2.建立会话

2. 建立会话
```js
// store需要自行实现,可参考最后的
const store   = new MySignalProtocolStore(); // 储存identity, prekeys, signed prekeys, and session state
const address = new libsignal.SignalProtocolAddress(recipientId, deviceId); // 目标地址

// Instantiate a SessionBuilder for a remote recipientId + deviceId tuple.
const sessionBuilder = new libsignal.SessionBuilder(store, address);

// 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.
// 将载入时生成的id和key传入以建立链接
const promise = sessionBuilder.processPreKey({
    registrationId: <Number>,
    identityKey: <ArrayBuffer>,
    signedPreKey: {
        keyId     : <Number>,
        publicKey : <ArrayBuffer>,
        signature : <ArrayBuffer>
    },
    preKey: {
        keyId     : <Number>,
        publicKey : <ArrayBuffer>
    }
});

promise.then(function onsuccess() {
  // encrypt messages
});

promise.catch(function onerror(error) {
  // handle identity key conflict
});
  1. 加密
    const plaintext = "Hello world"; // 要发送的原信息
    const sessionCipher = new libsignal.SessionCipher(store, address); // 加密解密接口
    sessionCipher.encrypt(plaintext).then(function(ciphertext) {
    // ciphertext -> { type: <Number>, body: <string> }
    handle(ciphertext.type, ciphertext.body);
    });
  2. 解密
    
    // 新建会话解密PreKeyWhisperMessage
    const sessionCipher = new SessionCipher(store, address);
    // 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.
    sessionCipher.decryptPreKeyWhisperMessage(ciphertext).then(function(plaintext) {
    // handle plaintext ArrayBuffer
    }).catch(function(error) {
    // handle identity key conflict
    });

// 使用现有会话解密WhisperMessage // Decrypt a normal message using an existing session const sessionCipher = new SessionCipher(store, address); sessionCipher.decryptWhisperMessage(ciphertext).then(function(plaintext) { // handle plaintext ArrayBuffer });


stroe示例,储存在memory中。但规范要求持久化(durable)储存。
> 此示例来自[该js库的test](https://github.com/signalapp/libsignal-protocol-javascript/blob/master/test/InMemorySignalProtocolStore.js)
```js
function SignalProtocolStore() {
  this.store = {};
}

SignalProtocolStore.prototype = {
  Direction: {
    SENDING: 1,
    RECEIVING: 2,
  },

  getIdentityKeyPair: function() {
    return Promise.resolve(this.get('identityKey'));
  },
  getLocalRegistrationId: function() {
    return Promise.resolve(this.get('registrationId'));
  },
  put: function(key, value) {
    if (key === undefined || value === undefined || key === null || value === null)
      throw new Error("Tried to store undefined/null");
    this.store[key] = value;
  },
  get: function(key, defaultValue) {
    if (key === null || key === undefined)
      throw new Error("Tried to get value for undefined/null key");
    if (key in this.store) {
      return this.store[key];
    } else {
      return defaultValue;
    }
  },
  remove: function(key) {
    if (key === null || key === undefined)
      throw new Error("Tried to remove value for undefined/null key");
    delete this.store[key];
  },

  isTrustedIdentity: function(identifier, identityKey, direction) {
    if (identifier === null || identifier === undefined) {
      throw new Error("tried to check identity key for undefined/null key");
    }
    if (!(identityKey instanceof ArrayBuffer)) {
      throw new Error("Expected identityKey to be an ArrayBuffer");
    }
    var trusted = this.get('identityKey' + identifier);
    if (trusted === undefined) {
      return Promise.resolve(true);
    }
    return Promise.resolve(util.toString(identityKey) === util.toString(trusted));
  },
  loadIdentityKey: function(identifier) {
    if (identifier === null || identifier === undefined)
      throw new Error("Tried to get identity key for undefined/null key");
    return Promise.resolve(this.get('identityKey' + identifier));
  },
  saveIdentity: function(identifier, identityKey) {
    if (identifier === null || identifier === undefined)
      throw new Error("Tried to put identity key for undefined/null key");

    var address = new libsignal.SignalProtocolAddress.fromString(identifier);

    var existing = this.get('identityKey' + address.getName());
    this.put('identityKey' + address.getName(), identityKey)

    if (existing && util.toString(identityKey) !== util.toString(existing)) {
      return Promise.resolve(true);
    } else {
      return Promise.resolve(false);
    }

  },

  /* Returns a prekeypair object or undefined */
  loadPreKey: function(keyId) {
    var res = this.get('25519KeypreKey' + keyId);
    if (res !== undefined) {
      res = { pubKey: res.pubKey, privKey: res.privKey };
    }
    return Promise.resolve(res);
  },
  storePreKey: function(keyId, keyPair) {
    return Promise.resolve(this.put('25519KeypreKey' + keyId, keyPair));
  },
  removePreKey: function(keyId) {
    return Promise.resolve(this.remove('25519KeypreKey' + keyId));
  },

  /* Returns a signed keypair object or undefined */
  // 25519.Curve25519是一个椭圆曲线,在加密中使用
  loadSignedPreKey: function(keyId) {
    var res = this.get('25519KeysignedKey' + keyId);
    if (res !== undefined) {
      res = { pubKey: res.pubKey, privKey: res.privKey };
    }
    return Promise.resolve(res);
  },
  storeSignedPreKey: function(keyId, keyPair) {
    return Promise.resolve(this.put('25519KeysignedKey' + keyId, keyPair));
  },
  removeSignedPreKey: function(keyId) {
    return Promise.resolve(this.remove('25519KeysignedKey' + keyId));
  },

  loadSession: function(identifier) {
    return Promise.resolve(this.get('session' + identifier));
  },
  storeSession: function(identifier, record) {
    return Promise.resolve(this.put('session' + identifier, record));
  },
  removeSession: function(identifier) {
    return Promise.resolve(this.remove('session' + identifier));
  },
  removeAllSessions: function(identifier) {
    for (var id in this.store) {
      if (id.startsWith('session' + identifier)) {
        delete this.store[id];
      }
    }
    return Promise.resolve();
  }
};
dzcpy commented 3 years ago

非常不错的文章!想请教下群聊如何做 e2e 加密呢?

1111mp commented 3 years ago

请问有完整的使用demo吗 我搞了很久 一点头绪都没有 直接把这些代码拷过来执行 很多报错 比如

KeyHelper.generatePreKey(keyId).then(function (preKey) {
        store.storePreKey(preKey.keyId, preKey.keyPair);
    });

这里的keyId是从哪里来的 是需要自己提供 还是怎么样

z-950 commented 3 years ago

@dzcpy 没有做过群聊加密。js的实现库内缺少群聊加密的方法。但其java版本有,可以参考然后自行实现。

z-950 commented 3 years ago

@1111mp 没有demo。此文写于一年前。按照我的记忆和查看依赖库的部分代码,keyId是自行生成的,不同用户的keyId可以重复,keyId主要用于存取key。

1111mp commented 3 years ago

@z-950 谢谢回复。刚入手,用都不会用。。。不过在社区找到了一个大佬的帖子,应该有帮助。 @dzcpy https://community.signalusers.org/t/an-unofficial-signal-chatbot-and-javascript-library/4767 群聊的也实现了 目前我还是没有入门成功 唉 但是他是封装好的 用的signal的服务 还是得自己实现

z-950 commented 3 years ago

@1111mp 运行例子确实难找。实际上难点还有后端部署和网络交互接口。客户端方面也许还可以参考测试代码。

1111mp commented 3 years ago

@z-950 这个不能单纯的用来做消息的加密和解密吗?比如我有自己的IM服务 现在只差一个端到端加密 我就想用这个加密一下 发送的消息的字符串。这样可行吗?

z-950 commented 3 years ago

@1111mp 因为我没有完整实现过,所以不能肯定的告诉你是否可以。但是理论上,后端也需要相关的功能。端到端加密为了安全性,设计了特定的密/公钥交换规则,后端一定需要实现这部分内容,这要求你对这些规则有所了解。自然,我不太清楚全部的流程。Signal有文档,你可以结合着它后端的例子看。 如果你不需要如此高的安全性,你大可以自行实现一种协议。

1111mp commented 3 years ago

@z-950 官方的文档就是看不懂啊 写的都一笔带过 然后自己就在摸索 根据这个人的用法 issues32 目前大概知道怎么去使用了 服务器需要保存一些pubKey 然后客户端根据这个pubKey和自己的priKey 建立回话 然后加密解密可以做到 但是官方文档说 ‘客户端会生成单个已签名的PreKey以及大量未签名的PreKey,并将它们全部传输到服务器。’这个不是很懂

const preKey = await KeyHelper.generatePreKey(keyId);

这个生成的preKey 本身就已经做到了吗 还是说需要执行大量的这个方法去存到服务器 然后目前只简单了解到这里了 群聊的 想都不敢想

z-950 commented 3 years ago

这个生成的preKey 本身就已经做到了吗 还是说需要执行大量的这个方法去存到服务器

@1111mp 每次只生成一个preKey。自己按需生成。

1111mp commented 3 years ago

每次只生成一个preKey。自己按需生成。 @z-950 可以这样理解吗

const promise = sessionBuilder.processPreKey({
registrationId,
identityKey: identityKeyPair.pubKey,
signedPreKey: {
...signedPreKey,
publicKey: signedPreKey.keyPair.pubKey
},
preKey: {
...preKey,
publicKey: preKey.keyPair.pubKey
}
});

验证的时候需要的这些参数 都是在跟客户端对应的 安装客户端的时候生成一次就行?

1111mp commented 3 years ago

@z-950

<!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>
    <script>
        var KeyHelper = libsignal.KeyHelper;
        var store = new SignalProtocolStore();

        generateKeys(123, function (aliceKeys) {
            store.put('identityKey', aliceKeys.identityKeyPair);
            store.put('registrationId', aliceKeys.registrationId);
            console.log('aliceKeys.registrationId', aliceKeys.registrationId)

            generateKeys(456, function (bobKeys) {

                console.log('bobKeys.registrationId', bobKeys.registrationId)

                var recipientId = "daniel123";
                var deviceId = 0;
                var address = new libsignal.SignalProtocolAddress(recipientId, deviceId);

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

                // 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: bobKeys.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);
                });

                const plaintext = "Hello world";
                // let ciphertext;
                const sessionCipher = new libsignal.SessionCipher(store, address);
                console.log(sessionCipher)
                sessionCipher.encrypt(plaintext).then(function (ciphertext) {
                    // ciphertext -> { type: <Number>, body: <string> }
                    console.log('ciphertext:', ciphertext)
                    // ciphertext = ciphertext;
                    // handle(ciphertext.type, ciphertext.body);

                    // var sessionCipher = new libsignal.SessionCipher(store, address);
                    // sessionCipher.decryptWhisperMessage(ciphertext.body).then(function (plaintext) {
                    //  // handle plaintext ArrayBuffer
                    //  console.log(plaintext)
                    // });

                    var addressCopy = new libsignal.SignalProtocolAddress(recipientId, deviceId);
                    var sessionCipherCopy = new libsignal.SessionCipher(store, addressCopy);

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

                });
            });

        });

        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);
                    });
                });
            });

        }
    </script>
</body>

</html>

我这么使用的时候 加密成功了 解密一直失败 解密的时候

var addressCopy = new libsignal.SignalProtocolAddress(recipientId, deviceId);
                    var sessionCipherCopy = new libsignal.SessionCipher(store, addressCopy);

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

addressCopy的recipientId和deviceId不是用的同一个吗 能指教一下吗

z-950 commented 3 years ago

验证的时候需要的这些参数 都是在跟客户端对应的 安装客户端的时候生成一次就行?

@1111mp 按照介绍,安装时都需要生成,并且持久化储存。后续聊天需要使用新生成的preKey。

1111mp commented 3 years ago

@z-950 好的 谢谢 解密刚刚已经成功了 https://github.com/signalapp/libsignal-protocol-javascript/issues/41 非常感谢

1111mp commented 3 years ago

@z-950 就是 每发一条消息 都需要生成 一个preKey 每个消息的preKey都不一样 这样吗

dzcpy commented 3 years ago

各位如果感兴趣的话,不如拉个群一起研究?这块我肯定是要实现一套方案出来的。这样多个人还能多点思路,简化开发

1111mp commented 3 years ago

@dzcpy 我自己刚建了一个qq群,691383606 有时间一起沟通下

1111mp commented 3 years ago

@dzcpy 看你也没有回复,我在这里分享一下,我自己理解的一种端到端加密的方案: simple_signal 跟signal protocal的安全性肯定没法比,但是它实在太难了,而且几乎没有相关的基础的文档,太费劲了。有点力不从心。 不过在整理这个的时候,我好想对signal protocol有了更深的理解,后续我看能不能整理出一个完整的简单的从0到1的例子出来。祝我这段时间少掉点头发吧。唉。

GavinZJM commented 1 year ago

想问下 加密解密都通了 但是这里app说 要本地存储session 用来存储棘轮的状态 方便未读消息解密 那这里拿到session 要怎么把他嵌入 原生代码 逻辑里 能否提供下

xie392 commented 6 months ago

想问下 加密解密都通了 但是这里app说 要本地存储session 用来存储棘轮的状态 方便未读消息解密 那这里拿到session 要怎么把他嵌入 原生代码 逻辑里 能否提供下 你解决了吗,我加密解密都行,但是不知道怎么存session,现在每次进去都会重新建立一个会话,导致之前的消息无法解密出来,只能解密当前会话的消息,这个会话如何存储?又怎么恢复?这是我的一个简单demo