mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2025-01-22 10:11:39 +01:00
Merge pull request #1042 from vector-im/cross-signing/self-sign-other-device
Cross-signing: sign other users
This commit is contained in:
commit
117ee3ba71
@ -54,6 +54,14 @@ export class MemberDetailsViewModel extends ViewModel {
|
||||
this.emitChange("role");
|
||||
}
|
||||
|
||||
async signUser() {
|
||||
if (this._session.crossSigning) {
|
||||
await this.logger.run("MemberDetailsViewModel.signUser", async log => {
|
||||
await this._session.crossSigning.signUser(this.userId, log);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get avatarLetter() {
|
||||
return avatarInitials(this.name);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {OLM_ALGORITHM} from "./e2ee/common.js";
|
||||
import {OLM_ALGORITHM} from "./e2ee/common";
|
||||
import {countBy, groupBy} from "../utils/groupBy";
|
||||
import {LRUCache} from "../utils/LRUCache";
|
||||
|
||||
|
@ -33,9 +33,9 @@ import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader";
|
||||
import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup";
|
||||
import {CrossSigning} from "./verification/CrossSigning";
|
||||
import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js";
|
||||
import {MEGOLM_ALGORITHM} from "./e2ee/common.js";
|
||||
import {MEGOLM_ALGORITHM} from "./e2ee/common";
|
||||
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
|
||||
import {DeviceTracker} from "./e2ee/DeviceTracker.js";
|
||||
import {DeviceTracker} from "./e2ee/DeviceTracker";
|
||||
import {LockMap} from "../utils/LockMap";
|
||||
import {groupBy} from "../utils/groupBy";
|
||||
import {
|
||||
|
@ -218,7 +218,7 @@ export class Sync {
|
||||
_openPrepareSyncTxn() {
|
||||
const storeNames = this._storage.storeNames;
|
||||
return this._storage.readTxn([
|
||||
storeNames.deviceIdentities, // to read device from olm messages
|
||||
storeNames.deviceKeys, // to read device from olm messages
|
||||
storeNames.olmSessions,
|
||||
storeNames.inboundGroupSessions,
|
||||
// to read fragments when loading sync writer when rejoining archived room
|
||||
@ -329,7 +329,7 @@ export class Sync {
|
||||
storeNames.pendingEvents,
|
||||
storeNames.userIdentities,
|
||||
storeNames.groupSessionDecryptions,
|
||||
storeNames.deviceIdentities,
|
||||
storeNames.deviceKeys,
|
||||
// to discard outbound session when somebody leaves a room
|
||||
// and to create room key messages when somebody joins
|
||||
storeNames.outboundGroupSessions,
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import {SESSION_E2EE_KEY_PREFIX, OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./common.js";
|
||||
import {SESSION_E2EE_KEY_PREFIX, OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./common";
|
||||
|
||||
// use common prefix so it's easy to clear properties that are not e2ee related during session clear
|
||||
const ACCOUNT_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "olmAccount";
|
||||
@ -259,7 +259,7 @@ export class Account {
|
||||
return obj;
|
||||
}
|
||||
|
||||
getDeviceKeysToSignWithCrossSigning() {
|
||||
getUnsignedDeviceKey() {
|
||||
const identityKeys = JSON.parse(this._account.identity_keys());
|
||||
return this._keysAsSignableObject(identityKeys);
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ limitations under the License.
|
||||
* see DeviceTracker
|
||||
*/
|
||||
|
||||
import type {DeviceIdentity} from "../storage/idb/stores/DeviceIdentityStore";
|
||||
import {getDeviceEd25519Key} from "./common";
|
||||
import type {DeviceKey} from "./common";
|
||||
import type {TimelineEvent} from "../storage/types";
|
||||
|
||||
type DecryptedEvent = {
|
||||
@ -35,7 +36,7 @@ type DecryptedEvent = {
|
||||
}
|
||||
|
||||
export class DecryptionResult {
|
||||
private device?: DeviceIdentity;
|
||||
private device?: DeviceKey;
|
||||
|
||||
constructor(
|
||||
public readonly event: DecryptedEvent,
|
||||
@ -44,13 +45,13 @@ export class DecryptionResult {
|
||||
public readonly encryptedEvent?: TimelineEvent
|
||||
) {}
|
||||
|
||||
setDevice(device: DeviceIdentity): void {
|
||||
setDevice(device: DeviceKey): void {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
get isVerified(): boolean {
|
||||
if (this.device) {
|
||||
const comesFromDevice = this.device.ed25519Key === this.claimedEd25519Key;
|
||||
const comesFromDevice = getDeviceEd25519Key(this.device) === this.claimedEd25519Key;
|
||||
return comesFromDevice;
|
||||
}
|
||||
return false;
|
||||
@ -65,11 +66,11 @@ export class DecryptionResult {
|
||||
}
|
||||
|
||||
get userId(): string | undefined {
|
||||
return this.device?.userId;
|
||||
return this.device?.user_id;
|
||||
}
|
||||
|
||||
get deviceId(): string | undefined {
|
||||
return this.device?.deviceId;
|
||||
return this.device?.device_id;
|
||||
}
|
||||
|
||||
get isVerificationUnknown(): boolean {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MEGOLM_ALGORITHM, DecryptionSource} from "./common.js";
|
||||
import {MEGOLM_ALGORITHM, DecryptionSource} from "./common";
|
||||
import {groupEventsBySession} from "./megolm/decryption/utils";
|
||||
import {mergeMap} from "../../utils/mergeMap";
|
||||
import {groupBy} from "../../utils/groupBy";
|
||||
@ -235,7 +235,7 @@ export class RoomEncryption {
|
||||
// Use devicesForUsers rather than devicesForRoomMembers as the room might not be tracked yet
|
||||
await this._deviceTracker.devicesForUsers(sendersWithoutDevice, hsApi, log);
|
||||
// now that we've fetched the missing devices, try verifying the results again
|
||||
const txn = await this._storage.readTxn([this._storage.storeNames.deviceIdentities]);
|
||||
const txn = await this._storage.readTxn([this._storage.storeNames.deviceKeys]);
|
||||
await this._verifyDecryptionResults(resultsWithoutDevice, txn);
|
||||
const resultsWithFoundDevice = resultsWithoutDevice.filter(r => !r.isVerificationUnknown);
|
||||
const resultsToEventIdMap = resultsWithFoundDevice.reduce((map, r) => {
|
||||
|
@ -15,9 +15,15 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import {createEnum} from "../../utils/enum";
|
||||
|
||||
export const DecryptionSource = createEnum("Sync", "Timeline", "Retry");
|
||||
import type {UnsentStateEvent} from "../room/common";
|
||||
import type {ILogItem} from "../../logging/types";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
type Olm = typeof OlmNamespace;
|
||||
|
||||
export enum DecryptionSource {
|
||||
Sync, Timeline, Retry
|
||||
};
|
||||
|
||||
// use common prefix so it's easy to clear properties that are not e2ee related during session clear
|
||||
export const SESSION_E2EE_KEY_PREFIX = "e2ee:";
|
||||
@ -25,26 +31,54 @@ export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
|
||||
export class DecryptionError extends Error {
|
||||
constructor(code, event, detailsObj = null) {
|
||||
constructor(private readonly code: string, private readonly event: object, private readonly detailsObj?: object) {
|
||||
super(`Decryption error ${code}${detailsObj ? ": "+JSON.stringify(detailsObj) : ""}`);
|
||||
this.code = code;
|
||||
this.event = event;
|
||||
this.details = detailsObj;
|
||||
}
|
||||
}
|
||||
|
||||
export const SIGNATURE_ALGORITHM = "ed25519";
|
||||
|
||||
export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Key, value, log = undefined) {
|
||||
const clone = Object.assign({}, value);
|
||||
delete clone.unsigned;
|
||||
delete clone.signatures;
|
||||
export type SignedValue = {
|
||||
signatures?: {[userId: string]: {[keyId: string]: string}}
|
||||
unsigned?: object
|
||||
}
|
||||
|
||||
// we store device keys (and cross-signing) in the format we get them from the server
|
||||
// as that is what the signature is calculated on, so to verify and sign, we need
|
||||
// it in this format anyway.
|
||||
export type DeviceKey = SignedValue & {
|
||||
readonly user_id: string;
|
||||
readonly device_id: string;
|
||||
readonly algorithms: ReadonlyArray<string>;
|
||||
readonly keys: {[keyId: string]: string};
|
||||
readonly unsigned: {
|
||||
device_display_name?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function getDeviceEd25519Key(deviceKey: DeviceKey): string {
|
||||
return deviceKey.keys[`ed25519:${deviceKey.device_id}`];
|
||||
}
|
||||
|
||||
export function getDeviceCurve25519Key(deviceKey: DeviceKey): string {
|
||||
return deviceKey.keys[`curve25519:${deviceKey.device_id}`];
|
||||
}
|
||||
|
||||
export function getEd25519Signature(signedValue: SignedValue, userId: string, deviceOrKeyId: string): string | undefined {
|
||||
return signedValue?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`];
|
||||
}
|
||||
|
||||
export function verifyEd25519Signature(olmUtil: Olm.Utility, userId: string, deviceOrKeyId: string, ed25519Key: string, value: SignedValue, log?: ILogItem) {
|
||||
const signature = getEd25519Signature(value, userId, deviceOrKeyId);
|
||||
if (!signature) {
|
||||
log?.set("no_signature", true);
|
||||
return false;
|
||||
}
|
||||
const clone = Object.assign({}, value) as object;
|
||||
delete clone["unsigned"];
|
||||
delete clone["signatures"];
|
||||
const canonicalJson = anotherjson.stringify(clone);
|
||||
const signature = value?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`];
|
||||
try {
|
||||
if (!signature) {
|
||||
throw new Error("no signature");
|
||||
}
|
||||
// throws when signature is invalid
|
||||
olmUtil.ed25519_verify(ed25519Key, canonicalJson, signature);
|
||||
return true;
|
||||
@ -58,7 +92,7 @@ export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Ke
|
||||
}
|
||||
}
|
||||
|
||||
export function createRoomEncryptionEvent() {
|
||||
export function createRoomEncryptionEvent(): UnsentStateEvent {
|
||||
return {
|
||||
"type": "m.room.encryption",
|
||||
"state_key": "",
|
||||
@ -70,16 +104,14 @@ export function createRoomEncryptionEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
export enum HistoryVisibility {
|
||||
Joined = "joined",
|
||||
Invited = "invited",
|
||||
WorldReadable = "world_readable",
|
||||
Shared = "shared",
|
||||
};
|
||||
|
||||
// Use enum when converting to TS
|
||||
export const HistoryVisibility = Object.freeze({
|
||||
Joined: "joined",
|
||||
Invited: "invited",
|
||||
WorldReadable: "world_readable",
|
||||
Shared: "shared",
|
||||
});
|
||||
|
||||
export function shouldShareKey(membership, historyVisibility) {
|
||||
export function shouldShareKey(membership: string, historyVisibility: HistoryVisibility) {
|
||||
switch (historyVisibility) {
|
||||
case HistoryVisibility.WorldReadable:
|
||||
return true;
|
@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {DecryptionError} from "../common.js";
|
||||
import {DecryptionPreparation} from "./decryption/DecryptionPreparation.js";
|
||||
import {SessionDecryption} from "./decryption/SessionDecryption";
|
||||
import {MEGOLM_ALGORITHM} from "../common.js";
|
||||
import {DecryptionError, MEGOLM_ALGORITHM} from "../common";
|
||||
import {validateEvent, groupEventsBySession} from "./decryption/utils";
|
||||
import {keyFromStorage, keyFromDeviceMessage, keyFromBackup} from "./decryption/RoomKey";
|
||||
import type {RoomKey, IncomingRoomKey} from "./decryption/RoomKey";
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MEGOLM_ALGORITHM} from "../common.js";
|
||||
import {MEGOLM_ALGORITHM} from "../common";
|
||||
import {OutboundRoomKey} from "./decryption/RoomKey";
|
||||
|
||||
export class Encryption {
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {DecryptionError} from "../../common.js";
|
||||
import {DecryptionError} from "../../common";
|
||||
|
||||
export class DecryptionChanges {
|
||||
constructor(roomId, results, errors, replayEntries) {
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {DecryptionResult} from "../../DecryptionResult";
|
||||
import {DecryptionError} from "../../common.js";
|
||||
import {DecryptionError} from "../../common";
|
||||
import {ReplayDetectionEntry} from "./ReplayDetectionEntry";
|
||||
import type {RoomKey} from "./RoomKey";
|
||||
import type {KeyLoader, OlmDecryptionResult} from "./KeyLoader";
|
||||
|
@ -42,7 +42,7 @@ export type SessionInfo = {
|
||||
}
|
||||
|
||||
export type MegOlmSessionKeyInfo = {
|
||||
algorithm: MEGOLM_ALGORITHM,
|
||||
algorithm: typeof MEGOLM_ALGORITHM,
|
||||
sender_key: string,
|
||||
sender_claimed_keys: {[algorithm: string]: string},
|
||||
forwarding_curve25519_key_chain: string[],
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {DecryptionError} from "../common.js";
|
||||
import {DecryptionError} from "../common";
|
||||
import {groupBy} from "../../../utils/groupBy";
|
||||
import {MultiLock, ILock} from "../../../utils/Lock";
|
||||
import {Session} from "./Session";
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {groupByWithCreator} from "../../../utils/groupBy";
|
||||
import {verifyEd25519Signature, OLM_ALGORITHM} from "../common.js";
|
||||
import {verifyEd25519Signature, OLM_ALGORITHM, getDeviceCurve25519Key, getDeviceEd25519Key} from "../common";
|
||||
import {createSessionEntry} from "./Session";
|
||||
|
||||
import type {OlmMessage, OlmPayload, OlmEncryptedMessageContent} from "./types";
|
||||
@ -24,7 +24,7 @@ import type {LockMap} from "../../../utils/LockMap";
|
||||
import {Lock, MultiLock, ILock} from "../../../utils/Lock";
|
||||
import type {Storage} from "../../storage/idb/Storage";
|
||||
import type {Transaction} from "../../storage/idb/Transaction";
|
||||
import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore";
|
||||
import type {DeviceKey} from "../common";
|
||||
import type {HomeServerApi} from "../../net/HomeServerApi";
|
||||
import type {ILogItem} from "../../../logging/types";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
@ -99,7 +99,7 @@ export class Encryption {
|
||||
return new MultiLock(locks);
|
||||
}
|
||||
|
||||
async encrypt(type: string, content: Record<string, any>, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise<EncryptedMessage[]> {
|
||||
async encrypt(type: string, content: Record<string, any>, devices: DeviceKey[], hsApi: HomeServerApi, log: ILogItem): Promise<EncryptedMessage[]> {
|
||||
let messages: EncryptedMessage[] = [];
|
||||
for (let i = 0; i < devices.length ; i += MAX_BATCH_SIZE) {
|
||||
const batchDevices = devices.slice(i, i + MAX_BATCH_SIZE);
|
||||
@ -115,12 +115,12 @@ export class Encryption {
|
||||
return messages;
|
||||
}
|
||||
|
||||
async _encryptForMaxDevices(type: string, content: Record<string, any>, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise<EncryptedMessage[]> {
|
||||
async _encryptForMaxDevices(type: string, content: Record<string, any>, devices: DeviceKey[], hsApi: HomeServerApi, log: ILogItem): Promise<EncryptedMessage[]> {
|
||||
// TODO: see if we can only hold some of the locks until after the /keys/claim call (if needed)
|
||||
// take a lock on all senderKeys so decryption and other calls to encrypt (should not happen)
|
||||
// don't modify the sessions at the same time
|
||||
const locks = await Promise.all(devices.map(device => {
|
||||
return this.senderKeyLock.takeLock(device.curve25519Key);
|
||||
return this.senderKeyLock.takeLock(getDeviceCurve25519Key(device));
|
||||
}));
|
||||
try {
|
||||
const {
|
||||
@ -158,10 +158,10 @@ export class Encryption {
|
||||
}
|
||||
}
|
||||
|
||||
async _findExistingSessions(devices: DeviceIdentity[]): Promise<{devicesWithoutSession: DeviceIdentity[], existingEncryptionTargets: EncryptionTarget[]}> {
|
||||
async _findExistingSessions(devices: DeviceKey[]): Promise<{devicesWithoutSession: DeviceKey[], existingEncryptionTargets: EncryptionTarget[]}> {
|
||||
const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]);
|
||||
const sessionIdsForDevice = await Promise.all(devices.map(async device => {
|
||||
return await txn.olmSessions.getSessionIds(device.curve25519Key);
|
||||
return await txn.olmSessions.getSessionIds(getDeviceCurve25519Key(device));
|
||||
}));
|
||||
const devicesWithoutSession = devices.filter((_, i) => {
|
||||
const sessionIds = sessionIdsForDevice[i];
|
||||
@ -184,36 +184,36 @@ export class Encryption {
|
||||
const plaintext = JSON.stringify(this._buildPlainTextMessageForDevice(type, content, device));
|
||||
const message = session!.encrypt(plaintext);
|
||||
const encryptedContent = {
|
||||
algorithm: OLM_ALGORITHM,
|
||||
algorithm: OLM_ALGORITHM as typeof OLM_ALGORITHM,
|
||||
sender_key: this.account.identityKeys.curve25519,
|
||||
ciphertext: {
|
||||
[device.curve25519Key]: message
|
||||
[getDeviceCurve25519Key(device)]: message
|
||||
}
|
||||
};
|
||||
return encryptedContent;
|
||||
}
|
||||
|
||||
_buildPlainTextMessageForDevice(type: string, content: Record<string, any>, device: DeviceIdentity): OlmPayload {
|
||||
_buildPlainTextMessageForDevice(type: string, content: Record<string, any>, device: DeviceKey): OlmPayload {
|
||||
return {
|
||||
keys: {
|
||||
"ed25519": this.account.identityKeys.ed25519
|
||||
},
|
||||
recipient_keys: {
|
||||
"ed25519": device.ed25519Key
|
||||
"ed25519": getDeviceEd25519Key(device)
|
||||
},
|
||||
recipient: device.userId,
|
||||
recipient: device.user_id,
|
||||
sender: this.ownUserId,
|
||||
content,
|
||||
type
|
||||
}
|
||||
}
|
||||
|
||||
async _createNewSessions(devicesWithoutSession: DeviceIdentity[], hsApi: HomeServerApi, timestamp: number, log: ILogItem): Promise<EncryptionTarget[]> {
|
||||
async _createNewSessions(devicesWithoutSession: DeviceKey[], hsApi: HomeServerApi, timestamp: number, log: ILogItem): Promise<EncryptionTarget[]> {
|
||||
const newEncryptionTargets = await log.wrap("claim", log => this._claimOneTimeKeys(hsApi, devicesWithoutSession, log));
|
||||
try {
|
||||
for (const target of newEncryptionTargets) {
|
||||
const {device, oneTimeKey} = target;
|
||||
target.session = await this.account.createOutboundOlmSession(device.curve25519Key, oneTimeKey);
|
||||
target.session = await this.account.createOutboundOlmSession(getDeviceCurve25519Key(device), oneTimeKey);
|
||||
}
|
||||
await this._storeSessions(newEncryptionTargets, timestamp);
|
||||
} catch (err) {
|
||||
@ -225,16 +225,16 @@ export class Encryption {
|
||||
return newEncryptionTargets;
|
||||
}
|
||||
|
||||
async _claimOneTimeKeys(hsApi: HomeServerApi, deviceIdentities: DeviceIdentity[], log: ILogItem): Promise<EncryptionTarget[]> {
|
||||
async _claimOneTimeKeys(hsApi: HomeServerApi, deviceIdentities: DeviceKey[], log: ILogItem): Promise<EncryptionTarget[]> {
|
||||
// create a Map<userId, Map<deviceId, deviceIdentity>>
|
||||
const devicesByUser = groupByWithCreator(deviceIdentities,
|
||||
(device: DeviceIdentity) => device.userId,
|
||||
(): Map<string, DeviceIdentity> => new Map(),
|
||||
(deviceMap: Map<string, DeviceIdentity>, device: DeviceIdentity) => deviceMap.set(device.deviceId, device)
|
||||
(device: DeviceKey) => device.user_id,
|
||||
(): Map<string, DeviceKey> => new Map(),
|
||||
(deviceMap: Map<string, DeviceKey>, device: DeviceKey) => deviceMap.set(device.device_id, device)
|
||||
);
|
||||
const oneTimeKeys = Array.from(devicesByUser.entries()).reduce((usersObj, [userId, deviceMap]) => {
|
||||
usersObj[userId] = Array.from(deviceMap.values()).reduce((devicesObj, device) => {
|
||||
devicesObj[device.deviceId] = OTK_ALGORITHM;
|
||||
devicesObj[device.device_id] = OTK_ALGORITHM;
|
||||
return devicesObj;
|
||||
}, {});
|
||||
return usersObj;
|
||||
@ -250,7 +250,7 @@ export class Encryption {
|
||||
return this._verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log);
|
||||
}
|
||||
|
||||
_verifyAndCreateOTKTargets(userKeyMap: ClaimedOTKResponse, devicesByUser: Map<string, Map<string, DeviceIdentity>>, log: ILogItem): EncryptionTarget[] {
|
||||
_verifyAndCreateOTKTargets(userKeyMap: ClaimedOTKResponse, devicesByUser: Map<string, Map<string, DeviceKey>>, log: ILogItem): EncryptionTarget[] {
|
||||
const verifiedEncryptionTargets: EncryptionTarget[] = [];
|
||||
for (const [userId, userSection] of Object.entries(userKeyMap)) {
|
||||
for (const [deviceId, deviceSection] of Object.entries(userSection)) {
|
||||
@ -260,7 +260,7 @@ export class Encryption {
|
||||
const device = devicesByUser.get(userId)?.get(deviceId);
|
||||
if (device) {
|
||||
const isValidSignature = verifyEd25519Signature(
|
||||
this.olmUtil, userId, deviceId, device.ed25519Key, keySection, log);
|
||||
this.olmUtil, userId, deviceId, getDeviceEd25519Key(device), keySection, log);
|
||||
if (isValidSignature) {
|
||||
const target = EncryptionTarget.fromOTK(device, keySection.key);
|
||||
verifiedEncryptionTargets.push(target);
|
||||
@ -281,7 +281,7 @@ export class Encryption {
|
||||
try {
|
||||
await Promise.all(encryptionTargets.map(async encryptionTarget => {
|
||||
const sessionEntry = await txn.olmSessions.get(
|
||||
encryptionTarget.device.curve25519Key, encryptionTarget.sessionId!);
|
||||
getDeviceCurve25519Key(encryptionTarget.device), encryptionTarget.sessionId!);
|
||||
if (sessionEntry && !failed) {
|
||||
const olmSession = new this.olm.Session();
|
||||
olmSession.unpickle(this.pickleKey, sessionEntry.session);
|
||||
@ -303,7 +303,7 @@ export class Encryption {
|
||||
try {
|
||||
for (const target of encryptionTargets) {
|
||||
const sessionEntry = createSessionEntry(
|
||||
target.session!, target.device.curve25519Key, timestamp, this.pickleKey);
|
||||
target.session!, getDeviceCurve25519Key(target.device), timestamp, this.pickleKey);
|
||||
txn.olmSessions.set(sessionEntry);
|
||||
}
|
||||
} catch (err) {
|
||||
@ -323,16 +323,16 @@ class EncryptionTarget {
|
||||
public session: Olm.Session | null = null;
|
||||
|
||||
constructor(
|
||||
public readonly device: DeviceIdentity,
|
||||
public readonly device: DeviceKey,
|
||||
public readonly oneTimeKey: string | null,
|
||||
public readonly sessionId: string | null
|
||||
) {}
|
||||
|
||||
static fromOTK(device: DeviceIdentity, oneTimeKey: string): EncryptionTarget {
|
||||
static fromOTK(device: DeviceKey, oneTimeKey: string): EncryptionTarget {
|
||||
return new EncryptionTarget(device, oneTimeKey, null);
|
||||
}
|
||||
|
||||
static fromSessionId(device: DeviceIdentity, sessionId: string): EncryptionTarget {
|
||||
static fromSessionId(device: DeviceKey, sessionId: string): EncryptionTarget {
|
||||
return new EncryptionTarget(device, null, sessionId);
|
||||
}
|
||||
|
||||
@ -346,6 +346,6 @@ class EncryptionTarget {
|
||||
export class EncryptedMessage {
|
||||
constructor(
|
||||
public readonly content: OlmEncryptedMessageContent,
|
||||
public readonly device: DeviceIdentity
|
||||
public readonly device: DeviceKey
|
||||
) {}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type {OLM_ALGORITHM} from "../common";
|
||||
|
||||
export const enum OlmPayloadType {
|
||||
PreKey = 0,
|
||||
Normal = 1
|
||||
@ -25,7 +27,7 @@ export type OlmMessage = {
|
||||
}
|
||||
|
||||
export type OlmEncryptedMessageContent = {
|
||||
algorithm?: "m.olm.v1.curve25519-aes-sha2"
|
||||
algorithm?: typeof OLM_ALGORITHM
|
||||
sender_key?: string,
|
||||
ciphertext?: {
|
||||
[deviceCurve25519Key: string]: OlmMessage
|
||||
|
@ -26,7 +26,7 @@ import {MemberList} from "./members/MemberList.js";
|
||||
import {Heroes} from "./members/Heroes.js";
|
||||
import {EventEntry} from "./timeline/entries/EventEntry.js";
|
||||
import {ObservedEventMap} from "./ObservedEventMap.js";
|
||||
import {DecryptionSource} from "../e2ee/common.js";
|
||||
import {DecryptionSource} from "../e2ee/common";
|
||||
import {ensureLogItem} from "../../logging/utils";
|
||||
import {PowerLevels} from "./PowerLevels.js";
|
||||
import {RetainedObservableValue} from "../../observable/value";
|
||||
@ -173,7 +173,7 @@ export class BaseRoom extends EventEmitter {
|
||||
const isTimelineOpen = this._isTimelineOpen;
|
||||
if (isTimelineOpen) {
|
||||
// read to fetch devices if timeline is open
|
||||
stores.push(this._storage.storeNames.deviceIdentities);
|
||||
stores.push(this._storage.storeNames.deviceKeys);
|
||||
}
|
||||
const writeTxn = await this._storage.readWriteTxn(stores);
|
||||
let decryption;
|
||||
|
@ -22,7 +22,7 @@ import {SendQueue} from "./sending/SendQueue.js";
|
||||
import {WrappedError} from "../error.js"
|
||||
import {Heroes} from "./members/Heroes.js";
|
||||
import {AttachmentUpload} from "./AttachmentUpload.js";
|
||||
import {DecryptionSource} from "../e2ee/common.js";
|
||||
import {DecryptionSource} from "../e2ee/common";
|
||||
import {iterateResponseStateEvents} from "./common";
|
||||
import {PowerLevels, EVENT_TYPE as POWERLEVELS_EVENT_TYPE } from "./PowerLevels.js";
|
||||
|
||||
|
@ -20,7 +20,7 @@ import {MediaRepository} from "../net/MediaRepository";
|
||||
import {EventEmitter} from "../../utils/EventEmitter";
|
||||
import {AttachmentUpload} from "./AttachmentUpload";
|
||||
import {loadProfiles, Profile, UserIdProfile} from "../profile";
|
||||
import {RoomType} from "./common";
|
||||
import {RoomType, UnsentStateEvent} from "./common";
|
||||
|
||||
import type {HomeServerApi} from "../net/HomeServerApi";
|
||||
import type {ILogItem} from "../../logging/types";
|
||||
@ -37,7 +37,7 @@ type CreateRoomPayload = {
|
||||
invite?: string[];
|
||||
room_alias_name?: string;
|
||||
creation_content?: {"m.federate": boolean};
|
||||
initial_state: { type: string; state_key: string; content: Record<string, any> }[];
|
||||
initial_state: UnsentStateEvent[];
|
||||
power_level_content_override?: Record<string, any>;
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MEGOLM_ALGORITHM} from "../e2ee/common.js";
|
||||
import {MEGOLM_ALGORITHM} from "../e2ee/common";
|
||||
import {iterateResponseStateEvents} from "./common";
|
||||
|
||||
function applyTimelineEntries(data, timelineEntries, isInitialSync, canMarkUnread, ownUserId) {
|
||||
|
@ -28,6 +28,8 @@ export function isRedacted(event) {
|
||||
return !!event?.unsigned?.redacted_because;
|
||||
}
|
||||
|
||||
export type UnsentStateEvent = { type: string; state_key: string; content: Record<string, any> };
|
||||
|
||||
export enum RoomStatus {
|
||||
None = 1 << 0,
|
||||
BeingCreated = 1 << 1,
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
import {KeyDescription, Key} from "./common";
|
||||
import {keyFromPassphrase} from "./passphrase";
|
||||
import {keyFromRecoveryKey} from "./recoveryKey";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common";
|
||||
import type {Storage} from "../storage/idb/Storage";
|
||||
import type {Transaction} from "../storage/idb/Transaction";
|
||||
import type {KeyDescriptionData} from "./common";
|
||||
|
@ -26,14 +26,15 @@ export enum StoreNames {
|
||||
timelineFragments = "timelineFragments",
|
||||
pendingEvents = "pendingEvents",
|
||||
userIdentities = "userIdentities",
|
||||
deviceIdentities = "deviceIdentities",
|
||||
deviceKeys = "deviceKeys",
|
||||
olmSessions = "olmSessions",
|
||||
inboundGroupSessions = "inboundGroupSessions",
|
||||
outboundGroupSessions = "outboundGroupSessions",
|
||||
groupSessionDecryptions = "groupSessionDecryptions",
|
||||
operations = "operations",
|
||||
accountData = "accountData",
|
||||
calls = "calls"
|
||||
calls = "calls",
|
||||
crossSigningKeys = "crossSigningKeys"
|
||||
}
|
||||
|
||||
export const STORE_NAMES: Readonly<StoreNames[]> = Object.values(StoreNames);
|
||||
|
@ -29,7 +29,8 @@ import {RoomMemberStore} from "./stores/RoomMemberStore";
|
||||
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore";
|
||||
import {PendingEventStore} from "./stores/PendingEventStore";
|
||||
import {UserIdentityStore} from "./stores/UserIdentityStore";
|
||||
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore";
|
||||
import {DeviceKeyStore} from "./stores/DeviceKeyStore";
|
||||
import {CrossSigningKeyStore} from "./stores/CrossSigningKeyStore";
|
||||
import {OlmSessionStore} from "./stores/OlmSessionStore";
|
||||
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore";
|
||||
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
|
||||
@ -141,8 +142,12 @@ export class Transaction {
|
||||
return this._store(StoreNames.userIdentities, idbStore => new UserIdentityStore(idbStore));
|
||||
}
|
||||
|
||||
get deviceIdentities(): DeviceIdentityStore {
|
||||
return this._store(StoreNames.deviceIdentities, idbStore => new DeviceIdentityStore(idbStore));
|
||||
get deviceKeys(): DeviceKeyStore {
|
||||
return this._store(StoreNames.deviceKeys, idbStore => new DeviceKeyStore(idbStore));
|
||||
}
|
||||
|
||||
get crossSigningKeys(): CrossSigningKeyStore {
|
||||
return this._store(StoreNames.crossSigningKeys, idbStore => new CrossSigningKeyStore(idbStore));
|
||||
}
|
||||
|
||||
get olmSessions(): OlmSessionStore {
|
||||
|
@ -2,7 +2,7 @@ import {IDOMStorage} from "./types";
|
||||
import {ITransaction} from "./QueryTarget";
|
||||
import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils";
|
||||
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common";
|
||||
import {SummaryData} from "../../room/RoomSummary";
|
||||
import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore";
|
||||
import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus, KeySource} from "./stores/InboundGroupSessionStore";
|
||||
@ -13,6 +13,8 @@ import {encodeScopeTypeKey} from "./stores/OperationStore";
|
||||
import {MAX_UNICODE} from "./stores/common";
|
||||
import {ILogItem} from "../../../logging/types";
|
||||
|
||||
import type {UserIdentity} from "../../e2ee/DeviceTracker";
|
||||
import {KeysTrackingStatus} from "../../e2ee/DeviceTracker";
|
||||
|
||||
export type MigrationFunc = (db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) => Promise<void> | void;
|
||||
// FUNCTIONS SHOULD ONLY BE APPENDED!!
|
||||
@ -34,7 +36,8 @@ export const schema: MigrationFunc[] = [
|
||||
clearAllStores,
|
||||
addInboundSessionBackupIndex,
|
||||
migrateBackupStatus,
|
||||
createCallStore
|
||||
createCallStore,
|
||||
applyCrossSigningChanges
|
||||
];
|
||||
// TODO: how to deal with git merge conflicts of this array?
|
||||
|
||||
@ -275,3 +278,24 @@ async function migrateBackupStatus(db: IDBDatabase, txn: IDBTransaction, localSt
|
||||
function createCallStore(db: IDBDatabase) : void {
|
||||
db.createObjectStore("calls", {keyPath: "key"});
|
||||
}
|
||||
|
||||
//v18 add crossSigningKeys store, rename deviceIdentities to deviceKeys and empties userIdentities
|
||||
async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) : Promise<void> {
|
||||
db.createObjectStore("crossSigningKeys", {keyPath: "key"});
|
||||
db.deleteObjectStore("deviceIdentities");
|
||||
const deviceKeys = db.createObjectStore("deviceKeys", {keyPath: "key"});
|
||||
deviceKeys.createIndex("byCurve25519Key", "curve25519Key", {unique: true});
|
||||
// mark all userIdentities as outdated as cross-signing keys won't be stored
|
||||
// also rename the deviceTrackingStatus field to keysTrackingStatus
|
||||
const userIdentities = txn.objectStore("userIdentities");
|
||||
let counter = 0;
|
||||
await iterateCursor<UserIdentity>(userIdentities.openCursor(), (value, key, cursor) => {
|
||||
delete value["deviceTrackingStatus"];
|
||||
delete value["crossSigningKeys"];
|
||||
value.keysTrackingStatus = KeysTrackingStatus.Outdated;
|
||||
cursor.update(value);
|
||||
counter += 1;
|
||||
return NOT_DONE;
|
||||
});
|
||||
log.set("marked_outdated", counter);
|
||||
}
|
||||
|
63
src/matrix/storage/idb/stores/CrossSigningKeyStore.ts
Normal file
63
src/matrix/storage/idb/stores/CrossSigningKeyStore.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MAX_UNICODE, MIN_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
import type {CrossSigningKey} from "../../../verification/CrossSigning";
|
||||
|
||||
type CrossSigningKeyEntry = {
|
||||
crossSigningKey: CrossSigningKey
|
||||
key: string; // key in storage, not a crypto key
|
||||
}
|
||||
|
||||
function encodeKey(userId: string, usage: string): string {
|
||||
return `${userId}|${usage}`;
|
||||
}
|
||||
|
||||
function decodeKey(key: string): { userId: string, usage: string } {
|
||||
const [userId, usage] = key.split("|");
|
||||
return {userId, usage};
|
||||
}
|
||||
|
||||
export class CrossSigningKeyStore {
|
||||
private _store: Store<CrossSigningKeyEntry>;
|
||||
|
||||
constructor(store: Store<CrossSigningKeyEntry>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
async get(userId: string, deviceId: string): Promise<CrossSigningKey | undefined> {
|
||||
return (await this._store.get(encodeKey(userId, deviceId)))?.crossSigningKey;
|
||||
}
|
||||
|
||||
set(crossSigningKey: CrossSigningKey): void {
|
||||
this._store.put({
|
||||
key:encodeKey(crossSigningKey["user_id"], crossSigningKey.usage[0]),
|
||||
crossSigningKey
|
||||
});
|
||||
}
|
||||
|
||||
remove(userId: string, usage: string): void {
|
||||
this._store.delete(encodeKey(userId, usage));
|
||||
}
|
||||
|
||||
removeAllForUser(userId: string): void {
|
||||
// exclude both keys as they are theoretical min and max,
|
||||
// but we should't have a match for just the room id, or room id with max
|
||||
const range = this._store.IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true);
|
||||
this._store.delete(range);
|
||||
}
|
||||
}
|
@ -16,15 +16,13 @@ limitations under the License.
|
||||
|
||||
import {MAX_UNICODE, MIN_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
import {getDeviceCurve25519Key} from "../../../e2ee/common";
|
||||
import type {DeviceKey} from "../../../e2ee/common";
|
||||
|
||||
export interface DeviceIdentity {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
ed25519Key: string;
|
||||
type DeviceKeyEntry = {
|
||||
key: string; // key in storage, not a crypto key
|
||||
curve25519Key: string;
|
||||
algorithms: string[];
|
||||
displayName: string;
|
||||
key: string;
|
||||
deviceKey: DeviceKey
|
||||
}
|
||||
|
||||
function encodeKey(userId: string, deviceId: string): string {
|
||||
@ -36,23 +34,24 @@ function decodeKey(key: string): { userId: string, deviceId: string } {
|
||||
return {userId, deviceId};
|
||||
}
|
||||
|
||||
export class DeviceIdentityStore {
|
||||
private _store: Store<DeviceIdentity>;
|
||||
export class DeviceKeyStore {
|
||||
private _store: Store<DeviceKeyEntry>;
|
||||
|
||||
constructor(store: Store<DeviceIdentity>) {
|
||||
constructor(store: Store<DeviceKeyEntry>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
getAllForUserId(userId: string): Promise<DeviceIdentity[]> {
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
||||
return this._store.selectWhile(range, device => {
|
||||
return device.userId === userId;
|
||||
async getAllForUserId(userId: string): Promise<DeviceKey[]> {
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, MIN_UNICODE));
|
||||
const entries = await this._store.selectWhile(range, device => {
|
||||
return device.deviceKey.user_id === userId;
|
||||
});
|
||||
return entries.map(e => e.deviceKey);
|
||||
}
|
||||
|
||||
async getAllDeviceIds(userId: string): Promise<string[]> {
|
||||
const deviceIds: string[] = [];
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, MIN_UNICODE));
|
||||
await this._store.iterateKeys(range, key => {
|
||||
const decodedKey = decodeKey(key as string);
|
||||
// prevent running into the next room
|
||||
@ -65,17 +64,21 @@ export class DeviceIdentityStore {
|
||||
return deviceIds;
|
||||
}
|
||||
|
||||
get(userId: string, deviceId: string): Promise<DeviceIdentity | undefined> {
|
||||
return this._store.get(encodeKey(userId, deviceId));
|
||||
async get(userId: string, deviceId: string): Promise<DeviceKey | undefined> {
|
||||
return (await this._store.get(encodeKey(userId, deviceId)))?.deviceKey;
|
||||
}
|
||||
|
||||
set(deviceIdentity: DeviceIdentity): void {
|
||||
deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId);
|
||||
this._store.put(deviceIdentity);
|
||||
set(deviceKey: DeviceKey): void {
|
||||
this._store.put({
|
||||
key: encodeKey(deviceKey.user_id, deviceKey.device_id),
|
||||
curve25519Key: getDeviceCurve25519Key(deviceKey)!,
|
||||
deviceKey
|
||||
});
|
||||
}
|
||||
|
||||
getByCurve25519Key(curve25519Key: string): Promise<DeviceIdentity | undefined> {
|
||||
return this._store.index("byCurve25519Key").get(curve25519Key);
|
||||
async getByCurve25519Key(curve25519Key: string): Promise<DeviceKey | undefined> {
|
||||
const entry = await this._store.index("byCurve25519Key").get(curve25519Key);
|
||||
return entry?.deviceKey;
|
||||
}
|
||||
|
||||
remove(userId: string, deviceId: string): void {
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
import {Store} from "../Store";
|
||||
import {IDOMStorage} from "../types";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../../../e2ee/common.js";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../../../e2ee/common";
|
||||
import {parse, stringify} from "../../../../utils/typedJSON";
|
||||
import type {ILogItem} from "../../../../logging/types";
|
||||
|
||||
|
@ -14,12 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import {Store} from "../Store";
|
||||
|
||||
interface UserIdentity {
|
||||
userId: string;
|
||||
roomIds: string[];
|
||||
deviceTrackingStatus: number;
|
||||
}
|
||||
import type {UserIdentity} from "../../../e2ee/DeviceTracker";
|
||||
|
||||
export class UserIdentityStore {
|
||||
private _store: Store<UserIdentity>;
|
||||
|
@ -14,19 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {ILogItem} from "../../logging/types";
|
||||
import {pkSign} from "./common";
|
||||
|
||||
import type {SecretStorage} from "../ssss/SecretStorage";
|
||||
import type {Storage} from "../storage/idb/Storage";
|
||||
import type {Platform} from "../../platform/web/Platform";
|
||||
import type {DeviceTracker} from "../e2ee/DeviceTracker";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
import type {HomeServerApi} from "../net/HomeServerApi";
|
||||
import type {Account} from "../e2ee/Account";
|
||||
import { ILogItem } from "../../lib";
|
||||
import {pkSign} from "./common";
|
||||
import type {ISignatures} from "./common";
|
||||
|
||||
import type {SignedValue, DeviceKey} from "../e2ee/common";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
type Olm = typeof OlmNamespace;
|
||||
|
||||
// we store cross-signing (and device) keys in the format we get them from the server
|
||||
// as that is what the signature is calculated on, so to verify and sign, we need
|
||||
// it in this format anyway.
|
||||
export type CrossSigningKey = SignedValue & {
|
||||
readonly user_id: string;
|
||||
readonly usage: ReadonlyArray<string>;
|
||||
readonly keys: {[keyId: string]: string};
|
||||
}
|
||||
|
||||
export enum KeyUsage {
|
||||
Master = "master",
|
||||
SelfSigning = "self_signing",
|
||||
UserSigning = "user_signing"
|
||||
};
|
||||
|
||||
export class CrossSigning {
|
||||
private readonly storage: Storage;
|
||||
private readonly secretStorage: SecretStorage;
|
||||
@ -62,51 +77,139 @@ export class CrossSigning {
|
||||
log.wrap("CrossSigning.init", async log => {
|
||||
// TODO: use errorboundary here
|
||||
const txn = await this.storage.readTxn([this.storage.storeNames.accountData]);
|
||||
|
||||
const mskSeed = await this.secretStorage.readSecret("m.cross_signing.master", txn);
|
||||
const privateMasterKey = await this.getSigningKey(KeyUsage.Master);
|
||||
const signing = new this.olm.PkSigning();
|
||||
let derivedPublicKey;
|
||||
try {
|
||||
const seed = new Uint8Array(this.platform.encoding.base64.decode(mskSeed));
|
||||
derivedPublicKey = signing.init_with_seed(seed);
|
||||
derivedPublicKey = signing.init_with_seed(privateMasterKey);
|
||||
} finally {
|
||||
signing.free();
|
||||
}
|
||||
const publishedKeys = await this.deviceTracker.getCrossSigningKeysForUser(this.ownUserId, this.hsApi, log);
|
||||
log.set({publishedMasterKey: publishedKeys.masterKey, derivedPublicKey});
|
||||
this._isMasterKeyTrusted = publishedKeys.masterKey === derivedPublicKey;
|
||||
const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log);
|
||||
const publisedEd25519Key = publishedMasterKey && getKeyEd25519Key(publishedMasterKey);
|
||||
log.set({publishedMasterKey: publisedEd25519Key, derivedPublicKey});
|
||||
this._isMasterKeyTrusted = !!publisedEd25519Key && publisedEd25519Key === derivedPublicKey;
|
||||
log.set("isMasterKeyTrusted", this.isMasterKeyTrusted);
|
||||
});
|
||||
}
|
||||
|
||||
async signOwnDevice(log: ILogItem) {
|
||||
log.wrap("CrossSigning.signOwnDevice", async log => {
|
||||
if (!this._isMasterKeyTrusted) {
|
||||
log.set("mskNotTrusted", true);
|
||||
return;
|
||||
}
|
||||
const deviceKey = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning();
|
||||
const signedDeviceKey = await this.signDevice(deviceKey);
|
||||
const payload = {
|
||||
[signedDeviceKey["user_id"]]: {
|
||||
[signedDeviceKey["device_id"]]: signedDeviceKey
|
||||
}
|
||||
};
|
||||
const request = this.hsApi.uploadSignatures(payload, {log});
|
||||
await request.response();
|
||||
});
|
||||
}
|
||||
|
||||
private async signDevice<T extends object>(data: T): Promise<T & { signatures: ISignatures }> {
|
||||
const txn = await this.storage.readTxn([this.storage.storeNames.accountData]);
|
||||
const seedStr = await this.secretStorage.readSecret(`m.cross_signing.self_signing`, txn);
|
||||
const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr));
|
||||
pkSign(this.olm, data, seed, this.ownUserId, "");
|
||||
return data as T & { signatures: ISignatures };
|
||||
}
|
||||
|
||||
get isMasterKeyTrusted(): boolean {
|
||||
return this._isMasterKeyTrusted;
|
||||
}
|
||||
|
||||
/** returns our own device key signed by our self-signing key. Other signatures will be missing. */
|
||||
async signOwnDevice(log: ILogItem): Promise<DeviceKey | undefined> {
|
||||
return log.wrap("CrossSigning.signOwnDevice", async log => {
|
||||
if (!this._isMasterKeyTrusted) {
|
||||
log.set("mskNotTrusted", true);
|
||||
return;
|
||||
}
|
||||
const ownDeviceKey = this.e2eeAccount.getUnsignedDeviceKey() as DeviceKey;
|
||||
return this.signDeviceKey(ownDeviceKey, log);
|
||||
});
|
||||
}
|
||||
|
||||
/** @return the signed device key for the given device id */
|
||||
async signDevice(deviceId: string, log: ILogItem): Promise<DeviceKey | undefined> {
|
||||
return log.wrap("CrossSigning.signDevice", async log => {
|
||||
log.set("id", deviceId);
|
||||
if (!this._isMasterKeyTrusted) {
|
||||
log.set("mskNotTrusted", true);
|
||||
return;
|
||||
}
|
||||
const keyToSign = await this.deviceTracker.deviceForId(this.ownUserId, deviceId, this.hsApi, log);
|
||||
if (!keyToSign) {
|
||||
return undefined;
|
||||
}
|
||||
delete keyToSign.signatures;
|
||||
return this.signDeviceKey(keyToSign, log);
|
||||
});
|
||||
}
|
||||
|
||||
/** @return the signed MSK for the given user id */
|
||||
async signUser(userId: string, log: ILogItem): Promise<CrossSigningKey | undefined> {
|
||||
return log.wrap("CrossSigning.signUser", async log => {
|
||||
log.set("id", userId);
|
||||
if (!this._isMasterKeyTrusted) {
|
||||
log.set("mskNotTrusted", true);
|
||||
return;
|
||||
}
|
||||
// can't sign own user
|
||||
if (userId === this.ownUserId) {
|
||||
return;
|
||||
}
|
||||
const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log);
|
||||
if (!keyToSign) {
|
||||
return undefined;
|
||||
}
|
||||
delete keyToSign.signatures;
|
||||
const signingKey = await this.getSigningKey(KeyUsage.UserSigning);
|
||||
// add signature to keyToSign
|
||||
this.signKey(keyToSign, signingKey);
|
||||
const payload = {
|
||||
[keyToSign.user_id]: {
|
||||
[getKeyEd25519Key(keyToSign)!]: keyToSign
|
||||
}
|
||||
};
|
||||
const request = this.hsApi.uploadSignatures(payload, {log});
|
||||
await request.response();
|
||||
return keyToSign;
|
||||
});
|
||||
}
|
||||
|
||||
private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise<DeviceKey> {
|
||||
const signingKey = await this.getSigningKey(KeyUsage.SelfSigning);
|
||||
// add signature to keyToSign
|
||||
this.signKey(keyToSign, signingKey);
|
||||
// so the payload format of a signature is a map from userid to key id of the signed key
|
||||
// (without the algoritm prefix though according to example, e.g. just device id or base 64 public key)
|
||||
// to the complete signed key with the signature of the signing key in the signatures section.
|
||||
const payload = {
|
||||
[keyToSign.user_id]: {
|
||||
[keyToSign.device_id]: keyToSign
|
||||
}
|
||||
};
|
||||
const request = this.hsApi.uploadSignatures(payload, {log});
|
||||
await request.response();
|
||||
return keyToSign;
|
||||
}
|
||||
|
||||
private async getSigningKey(usage: KeyUsage): Promise<Uint8Array> {
|
||||
const txn = await this.storage.readTxn([this.storage.storeNames.accountData]);
|
||||
const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`, txn);
|
||||
const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr));
|
||||
return seed;
|
||||
}
|
||||
|
||||
private signKey(keyToSign: DeviceKey | CrossSigningKey, signingKey: Uint8Array) {
|
||||
pkSign(this.olm, keyToSign, signingKey, this.ownUserId, "");
|
||||
}
|
||||
}
|
||||
|
||||
export function getKeyUsage(keyInfo: CrossSigningKey): KeyUsage | undefined {
|
||||
if (!Array.isArray(keyInfo.usage) || keyInfo.usage.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const usage = keyInfo.usage[0];
|
||||
if (usage !== KeyUsage.Master && usage !== KeyUsage.SelfSigning && usage !== KeyUsage.UserSigning) {
|
||||
return undefined;
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
const algorithm = "ed25519";
|
||||
const prefix = `${algorithm}:`;
|
||||
|
||||
export function getKeyEd25519Key(keyInfo: CrossSigningKey): string | undefined {
|
||||
const ed25519KeyIds = Object.keys(keyInfo.keys).filter(keyId => keyId.startsWith(prefix));
|
||||
if (ed25519KeyIds.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const keyId = ed25519KeyIds[0];
|
||||
const publicKey = keyInfo.keys[keyId];
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
export function getKeyUserId(keyInfo: CrossSigningKey): string | undefined {
|
||||
return keyInfo["user_id"];
|
||||
}
|
||||
|
@ -16,24 +16,10 @@ limitations under the License.
|
||||
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
import type {SignedValue} from "../e2ee/common";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
type Olm = typeof OlmNamespace;
|
||||
|
||||
export interface IObject {
|
||||
unsigned?: object;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export interface ISignatures {
|
||||
[entity: string]: {
|
||||
[keyId: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISigned {
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
// from matrix-js-sdk
|
||||
/**
|
||||
* Sign a JSON object using public key cryptography
|
||||
@ -45,7 +31,7 @@ export interface ISigned {
|
||||
* @param pubKey - The public key (ignored if key is a seed)
|
||||
* @returns the signature for the object
|
||||
*/
|
||||
export function pkSign(olmUtil: Olm, obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
|
||||
export function pkSign(olmUtil: Olm, obj: SignedValue, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
|
||||
let createdKey = false;
|
||||
if (key instanceof Uint8Array) {
|
||||
const keyObj = new olmUtil.PkSigning();
|
||||
@ -69,4 +55,4 @@ export interface ISigned {
|
||||
key.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1182,6 +1182,7 @@ button.RoomDetailsView_row::after {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.LazyListParent {
|
||||
|
@ -41,14 +41,22 @@ export class MemberDetailsView extends TemplateView {
|
||||
}
|
||||
|
||||
_createOptions(t, vm) {
|
||||
const options = [
|
||||
t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`),
|
||||
t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`)
|
||||
];
|
||||
if (vm.features.crossSigning) {
|
||||
const onClick = () => {
|
||||
if (confirm("You don't want to do this with any account but a test account. This will cross-sign this user without verifying their keys first. You won't be able to undo this apart from resetting your cross-signing keys.")) {
|
||||
vm.signUser();
|
||||
}
|
||||
};
|
||||
options.push(t.button({className: "text", onClick}, vm.i18n`Cross-sign user (DO NOT USE, TESTING ONLY)`))
|
||||
}
|
||||
return t.div({ className: "MemberDetailsView_section" },
|
||||
[
|
||||
t.div({className: "MemberDetailsView_label"}, vm.i18n`Options`),
|
||||
t.div({className: "MemberDetailsView_options"},
|
||||
[
|
||||
t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`),
|
||||
t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`)
|
||||
])
|
||||
t.div({className: "MemberDetailsView_options"}, options)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export class KeyBackupSettingsView extends TemplateView {
|
||||
}),
|
||||
t.if(vm => vm.canSignOwnDevice, t => {
|
||||
return t.button({
|
||||
onClick: disableTargetCallback(async evt => {
|
||||
onClick: disableTargetCallback(async () => {
|
||||
await vm.signOwnDevice();
|
||||
})
|
||||
}, "Sign own device");
|
||||
|
Loading…
x
Reference in New Issue
Block a user