Merge pull request #1042 from vector-im/cross-signing/self-sign-other-device

Cross-signing: sign other users
This commit is contained in:
Bruno Windels 2023-03-27 11:09:16 +02:00 committed by GitHub
commit 117ee3ba71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 726 additions and 446 deletions

View File

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

View File

@ -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";

View File

@ -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 {

View File

@ -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,

View File

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

View File

@ -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 {

View File

@ -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) => {

View File

@ -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;

View File

@ -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";

View File

@ -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 {

View File

@ -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) {

View File

@ -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";

View File

@ -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[],

View File

@ -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";

View File

@ -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
) {}
}

View File

@ -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

View File

@ -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;

View File

@ -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";

View File

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

View File

@ -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) {

View File

@ -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,

View File

@ -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";

View File

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

View File

@ -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 {

View File

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

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

View File

@ -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 {

View File

@ -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";

View File

@ -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>;

View File

@ -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"];
}

View File

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

View File

@ -1182,6 +1182,7 @@ button.RoomDetailsView_row::after {
border: none;
background: none;
cursor: pointer;
text-align: left;
}
.LazyListParent {

View File

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

View File

@ -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");