Merge pull request #597 from vector-im/ts-conversion-matrix-ssss
Convert matrix/ssss to typescript
This commit is contained in:
commit
dacdc1aec6
10 changed files with 94 additions and 46 deletions
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
import {KeyType} from "../matrix/ssss/index.js";
|
import {KeyType} from "../matrix/ssss/index";
|
||||||
import {Status} from "./session/settings/SessionBackupViewModel.js";
|
import {Status} from "./session/settings/SessionBackupViewModel.js";
|
||||||
|
|
||||||
export class AccountSetupViewModel extends ViewModel {
|
export class AccountSetupViewModel extends ViewModel {
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ViewModel} from "../../ViewModel.js";
|
import {ViewModel} from "../../ViewModel.js";
|
||||||
import {KeyType} from "../../../matrix/ssss/index.js";
|
import {KeyType} from "../../../matrix/ssss/index";
|
||||||
import {createEnum} from "../../../utils/enum";
|
import {createEnum} from "../../../utils/enum";
|
||||||
|
|
||||||
export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending");
|
export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending");
|
||||||
|
|
|
@ -42,8 +42,8 @@ import {
|
||||||
writeKey as ssssWriteKey,
|
writeKey as ssssWriteKey,
|
||||||
removeKey as ssssRemoveKey,
|
removeKey as ssssRemoveKey,
|
||||||
keyFromDehydratedDeviceKey as createSSSSKeyFromDehydratedDeviceKey
|
keyFromDehydratedDeviceKey as createSSSSKeyFromDehydratedDeviceKey
|
||||||
} from "./ssss/index.js";
|
} from "./ssss/index";
|
||||||
import {SecretStorage} from "./ssss/SecretStorage.js";
|
import {SecretStorage} from "./ssss/SecretStorage";
|
||||||
import {ObservableValue, RetainedObservableValue} from "../observable/ObservableValue";
|
import {ObservableValue, RetainedObservableValue} from "../observable/ObservableValue";
|
||||||
|
|
||||||
const PICKLE_KEY = "DEFAULT_KEY";
|
const PICKLE_KEY = "DEFAULT_KEY";
|
||||||
|
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEHYDRATION_LIBOLM_PICKLE_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
const DEHYDRATION_LIBOLM_PICKLE_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||||
import {KeyDescription} from "../ssss/common.js";
|
import {KeyDescription} from "../ssss/common";
|
||||||
import {keyFromCredentialAndDescription} from "../ssss/index.js";
|
import {keyFromCredentialAndDescription} from "../ssss/index";
|
||||||
|
|
||||||
export async function getDehydratedDevice(hsApi, olm, platform, log) {
|
export async function getDehydratedDevice(hsApi, olm, platform, log) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -13,19 +13,31 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import type {Key} from "./common";
|
||||||
|
import type {Platform} from "../../platform/web/Platform.js";
|
||||||
|
import type {Transaction} from "../storage/idb/Transaction";
|
||||||
|
|
||||||
|
type EncryptedData = {
|
||||||
|
iv: string;
|
||||||
|
ciphertext: string;
|
||||||
|
mac: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class SecretStorage {
|
export class SecretStorage {
|
||||||
constructor({key, platform}) {
|
private readonly _key: Key;
|
||||||
|
private readonly _platform: Platform;
|
||||||
|
|
||||||
|
constructor({key, platform}: {key: Key, platform: Platform}) {
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
async readSecret(name, txn) {
|
async readSecret(name: string, txn: Transaction): Promise<string | undefined> {
|
||||||
const accountData = await txn.accountData.get(name);
|
const accountData = await txn.accountData.get(name);
|
||||||
if (!accountData) {
|
if (!accountData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encryptedData = accountData?.content?.encrypted?.[this._key.id];
|
const encryptedData = accountData?.content?.encrypted?.[this._key.id] as EncryptedData;
|
||||||
if (!encryptedData) {
|
if (!encryptedData) {
|
||||||
throw new Error(`Secret ${accountData.type} is not encrypted for key ${this._key.id}`);
|
throw new Error(`Secret ${accountData.type} is not encrypted for key ${this._key.id}`);
|
||||||
}
|
}
|
||||||
|
@ -37,7 +49,7 @@ export class SecretStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _decryptAESSecret(type, encryptedData) {
|
async _decryptAESSecret(type: string, encryptedData: EncryptedData): Promise<string> {
|
||||||
const {base64, utf8} = this._platform.encoding;
|
const {base64, utf8} = this._platform.encoding;
|
||||||
// now derive the aes and mac key from the 4s key
|
// now derive the aes and mac key from the 4s key
|
||||||
const hkdfKey = await this._platform.crypto.derive.hkdf(
|
const hkdfKey = await this._platform.crypto.derive.hkdf(
|
|
@ -14,25 +14,42 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {Platform} from "../../platform/web/Platform.js";
|
||||||
|
|
||||||
|
export type KeyDescriptionData = {
|
||||||
|
algorithm: string;
|
||||||
|
passphrase: {
|
||||||
|
algorithm: string;
|
||||||
|
iterations: number;
|
||||||
|
salt: string;
|
||||||
|
bits?: number;
|
||||||
|
};
|
||||||
|
mac: string;
|
||||||
|
iv: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class KeyDescription {
|
export class KeyDescription {
|
||||||
constructor(id, keyDescription) {
|
private readonly _id: string;
|
||||||
|
private readonly _keyDescription: KeyDescriptionData;
|
||||||
|
|
||||||
|
constructor(id: string, keyDescription: KeyDescriptionData) {
|
||||||
this._id = id;
|
this._id = id;
|
||||||
this._keyDescription = keyDescription;
|
this._keyDescription = keyDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id(): string {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get passphraseParams() {
|
get passphraseParams(): KeyDescriptionData["passphrase"] {
|
||||||
return this._keyDescription?.passphrase;
|
return this._keyDescription?.passphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
get algorithm() {
|
get algorithm(): string {
|
||||||
return this._keyDescription?.algorithm;
|
return this._keyDescription?.algorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
async isCompatible(key, platform) {
|
async isCompatible(key: Key, platform: Platform): Promise<boolean> {
|
||||||
if (this.algorithm === "m.secret_storage.v1.aes-hmac-sha2") {
|
if (this.algorithm === "m.secret_storage.v1.aes-hmac-sha2") {
|
||||||
const kd = this._keyDescription;
|
const kd = this._keyDescription;
|
||||||
if (kd.mac) {
|
if (kd.mac) {
|
||||||
|
@ -53,33 +70,36 @@ export class KeyDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Key {
|
export class Key {
|
||||||
constructor(keyDescription, binaryKey) {
|
private readonly _keyDescription: KeyDescription;
|
||||||
|
private readonly _binaryKey: Uint8Array;
|
||||||
|
|
||||||
|
constructor(keyDescription: KeyDescription, binaryKey: Uint8Array) {
|
||||||
this._keyDescription = keyDescription;
|
this._keyDescription = keyDescription;
|
||||||
this._binaryKey = binaryKey;
|
this._binaryKey = binaryKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDescription(description) {
|
withDescription(description: KeyDescription): Key {
|
||||||
return new Key(description, this._binaryKey);
|
return new Key(description, this._binaryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
get description() {
|
get description(): KeyDescription {
|
||||||
return this._keyDescription;
|
return this._keyDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id(): string {
|
||||||
return this._keyDescription.id;
|
return this._keyDescription.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get binaryKey() {
|
get binaryKey(): Uint8Array {
|
||||||
return this._binaryKey;
|
return this._binaryKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
get algorithm() {
|
get algorithm(): string {
|
||||||
return this._keyDescription.algorithm;
|
return this._keyDescription.algorithm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function calculateKeyMac(key, ivStr, platform) {
|
async function calculateKeyMac(key: BufferSource, ivStr: string, platform: Platform): Promise<string> {
|
||||||
const {crypto, encoding} = platform;
|
const {crypto, encoding} = platform;
|
||||||
const {utf8, base64} = encoding;
|
const {utf8, base64} = encoding;
|
||||||
const {derive, aes, hmac} = crypto;
|
const {derive, aes, hmac} = crypto;
|
|
@ -14,17 +14,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {KeyDescription, Key} from "./common.js";
|
import {KeyDescription, Key} from "./common";
|
||||||
import {keyFromPassphrase} from "./passphrase.js";
|
import {keyFromPassphrase} from "./passphrase";
|
||||||
import {keyFromRecoveryKey} from "./recoveryKey.js";
|
import {keyFromRecoveryKey} from "./recoveryKey";
|
||||||
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js";
|
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js";
|
||||||
import {createEnum} from "../../utils/enum";
|
import type {Storage} from "../storage/idb/Storage";
|
||||||
|
import type {Transaction} from "../storage/idb/Transaction";
|
||||||
|
import type {KeyDescriptionData} from "./common";
|
||||||
|
import type {Platform} from "../../platform/web/Platform.js";
|
||||||
|
import type * as OlmNamespace from "@matrix-org/olm"
|
||||||
|
|
||||||
|
type Olm = typeof OlmNamespace;
|
||||||
|
|
||||||
const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`;
|
const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`;
|
||||||
|
|
||||||
export const KeyType = createEnum("RecoveryKey", "Passphrase");
|
export enum KeyType {
|
||||||
|
"RecoveryKey",
|
||||||
|
"Passphrase"
|
||||||
|
}
|
||||||
|
|
||||||
async function readDefaultKeyDescription(storage) {
|
async function readDefaultKeyDescription(storage: Storage): Promise<KeyDescription | undefined> {
|
||||||
const txn = await storage.readTxn([
|
const txn = await storage.readTxn([
|
||||||
storage.storeNames.accountData
|
storage.storeNames.accountData
|
||||||
]);
|
]);
|
||||||
|
@ -37,30 +46,30 @@ async function readDefaultKeyDescription(storage) {
|
||||||
if (!keyAccountData) {
|
if (!keyAccountData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return new KeyDescription(id, keyAccountData.content);
|
return new KeyDescription(id, keyAccountData.content as KeyDescriptionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeKey(key, txn) {
|
export async function writeKey(key: Key, txn: Transaction): Promise<void> {
|
||||||
txn.session.set(SSSS_KEY, {id: key.id, binaryKey: key.binaryKey});
|
txn.session.set(SSSS_KEY, {id: key.id, binaryKey: key.binaryKey});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readKey(txn) {
|
export async function readKey(txn: Transaction): Promise<Key | undefined> {
|
||||||
const keyData = await txn.session.get(SSSS_KEY);
|
const keyData = await txn.session.get(SSSS_KEY);
|
||||||
if (!keyData) {
|
if (!keyData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keyAccountData = await txn.accountData.get(`m.secret_storage.key.${keyData.id}`);
|
const keyAccountData = await txn.accountData.get(`m.secret_storage.key.${keyData.id}`);
|
||||||
if (keyAccountData) {
|
if (keyAccountData) {
|
||||||
return new Key(new KeyDescription(keyData.id, keyAccountData.content), keyData.binaryKey);
|
return new Key(new KeyDescription(keyData.id, keyAccountData.content as KeyDescriptionData), keyData.binaryKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function removeKey(txn) {
|
export async function removeKey(txn: Transaction): Promise<void> {
|
||||||
await txn.session.remove(SSSS_KEY);
|
txn.session.remove(SSSS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function keyFromCredential(type, credential, storage, platform, olm) {
|
export async function keyFromCredential(type: KeyType, credential: string, storage: Storage, platform: Platform, olm: Olm): Promise<Key> {
|
||||||
const keyDescription = await readDefaultKeyDescription(storage);
|
const keyDescription = await readDefaultKeyDescription(storage);
|
||||||
if (!keyDescription) {
|
if (!keyDescription) {
|
||||||
throw new Error("Could not find a default secret storage key in account data");
|
throw new Error("Could not find a default secret storage key in account data");
|
||||||
|
@ -68,8 +77,8 @@ export async function keyFromCredential(type, credential, storage, platform, olm
|
||||||
return await keyFromCredentialAndDescription(type, credential, keyDescription, platform, olm);
|
return await keyFromCredentialAndDescription(type, credential, keyDescription, platform, olm);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function keyFromCredentialAndDescription(type, credential, keyDescription, platform, olm) {
|
export async function keyFromCredentialAndDescription(type: KeyType, credential: string, keyDescription: KeyDescription, platform: Platform, olm: Olm): Promise<Key> {
|
||||||
let key;
|
let key: Key;
|
||||||
if (type === KeyType.Passphrase) {
|
if (type === KeyType.Passphrase) {
|
||||||
key = await keyFromPassphrase(keyDescription, credential, platform);
|
key = await keyFromPassphrase(keyDescription, credential, platform);
|
||||||
} else if (type === KeyType.RecoveryKey) {
|
} else if (type === KeyType.RecoveryKey) {
|
||||||
|
@ -80,9 +89,9 @@ export async function keyFromCredentialAndDescription(type, credential, keyDescr
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function keyFromDehydratedDeviceKey(key, storage, platform) {
|
export async function keyFromDehydratedDeviceKey(key: Key, storage: Storage, platform: Platform): Promise<Key | undefined> {
|
||||||
const keyDescription = await readDefaultKeyDescription(storage);
|
const keyDescription = await readDefaultKeyDescription(storage);
|
||||||
if (await keyDescription.isCompatible(key, platform)) {
|
if (await keyDescription?.isCompatible(key, platform)) {
|
||||||
return key.withDescription(keyDescription);
|
return key.withDescription(keyDescription!);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Key} from "./common.js";
|
import {Key} from "./common";
|
||||||
|
import type {KeyDescription} from "./common";
|
||||||
|
import type {Platform} from "../../platform/web/Platform.js";
|
||||||
|
|
||||||
const DEFAULT_ITERATIONS = 500000;
|
const DEFAULT_ITERATIONS = 500000;
|
||||||
const DEFAULT_BITSIZE = 256;
|
const DEFAULT_BITSIZE = 256;
|
||||||
|
@ -25,7 +27,7 @@ const DEFAULT_BITSIZE = 256;
|
||||||
* @param {Platform} platform
|
* @param {Platform} platform
|
||||||
* @return {Key}
|
* @return {Key}
|
||||||
*/
|
*/
|
||||||
export async function keyFromPassphrase(keyDescription, passphrase, platform) {
|
export async function keyFromPassphrase(keyDescription: KeyDescription, passphrase: string, platform: Platform): Promise<Key> {
|
||||||
const {passphraseParams} = keyDescription;
|
const {passphraseParams} = keyDescription;
|
||||||
if (!passphraseParams) {
|
if (!passphraseParams) {
|
||||||
throw new Error("not a passphrase key");
|
throw new Error("not a passphrase key");
|
|
@ -13,9 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import {Key} from "./common.js";
|
import {Key} from "./common";
|
||||||
|
import {KeyDescription} from "./common";
|
||||||
|
import type {Platform} from "../../platform/web/Platform.js";
|
||||||
|
import type * as OlmNamespace from "@matrix-org/olm";
|
||||||
|
type Olm = typeof OlmNamespace;
|
||||||
|
|
||||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Olm} olm
|
* @param {Olm} olm
|
||||||
|
@ -23,7 +27,7 @@ const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||||
* @param {string} recoveryKey
|
* @param {string} recoveryKey
|
||||||
* @return {Key}
|
* @return {Key}
|
||||||
*/
|
*/
|
||||||
export function keyFromRecoveryKey(keyDescription, recoveryKey, olm, platform) {
|
export function keyFromRecoveryKey(keyDescription: KeyDescription, recoveryKey: string, olm: Olm, platform: Platform): Key {
|
||||||
const result = platform.encoding.base58.decode(recoveryKey.replace(/ /g, ''));
|
const result = platform.encoding.base58.decode(recoveryKey.replace(/ /g, ''));
|
||||||
|
|
||||||
let parity = 0;
|
let parity = 0;
|
|
@ -2,7 +2,8 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"target": "ES2020"
|
"target": "ES2020",
|
||||||
|
"moduleResolution": "node"
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"src/sdk/paths/*"
|
"src/sdk/paths/*"
|
||||||
|
|
Reference in a new issue