Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signal协议 js使用 #5

Open
z-950 opened this issue Sep 7, 2020 · 20 comments
Open

Signal协议 js使用 #5

z-950 opened this issue Sep 7, 2020 · 20 comments
Labels
js javascript 前端

Comments

@z-950
Copy link
Owner

z-950 commented Sep 7, 2020

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

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

注意

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

概念

  • preKeys:ECPublicKey,唯一id
  • session:通信方式,两种:
    1. PreKeyBundles. 希望向接收者发送消息的客户端可以通过从服务器检索该接收者的PreKeyBundle来建立会话。
    2. PreKeySignalMessages. 客户端可以从收件人接收PreKeySignalMessage并使用它来建立会话

    一旦建立会话,就没有必要解除会话。

  • state:session期间client保存了大量state(持久化保存):
    • Identity State
    • PreKey State
    • Signed PreKey States
    • Session State

使用

引入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.建立会话
  1. 建立会话
// 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);
});
  1. 解密
// 新建会话解密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

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();
  }
};
@z-950 z-950 added 前端 js javascript labels Sep 7, 2020
@dzcpy
Copy link

dzcpy commented Nov 7, 2020

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

@1111mp
Copy link

1111mp commented Nov 18, 2020

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

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

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

@z-950
Copy link
Owner Author

z-950 commented Nov 18, 2020

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

@z-950
Copy link
Owner Author

z-950 commented Nov 18, 2020

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

@1111mp
Copy link

1111mp commented Nov 19, 2020

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

@z-950
Copy link
Owner Author

z-950 commented Nov 19, 2020

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

@1111mp
Copy link

1111mp commented Nov 19, 2020

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

@z-950
Copy link
Owner Author

z-950 commented Nov 19, 2020

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

@1111mp
Copy link

1111mp commented Nov 19, 2020

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

const preKey = await KeyHelper.generatePreKey(keyId);

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

@z-950
Copy link
Owner Author

z-950 commented Nov 19, 2020

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

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

@1111mp
Copy link

1111mp commented Nov 19, 2020

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

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

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

@1111mp
Copy link

1111mp commented Nov 19, 2020

@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
Copy link
Owner Author

z-950 commented Nov 19, 2020

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

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

@1111mp
Copy link

1111mp commented Nov 19, 2020

@z-950 好的 谢谢 解密刚刚已经成功了 signalapp/libsignal-protocol-javascript#41 非常感谢

@1111mp
Copy link

1111mp commented Nov 19, 2020

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

@dzcpy
Copy link

dzcpy commented Nov 19, 2020

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

@1111mp
Copy link

1111mp commented Nov 20, 2020

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

@1111mp
Copy link

1111mp commented Nov 20, 2020

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

@GavinZJM
Copy link

GavinZJM commented Nov 3, 2022

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

@xie392
Copy link

xie392 commented Jan 17, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
js javascript 前端
Projects
None yet
Development

No branches or pull requests

5 participants